From 8b994fb490a937d4564cd03e531d658a687006c7 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 19 Aug 2026 10:58:51 +0300 Subject: [PATCH 01/11] Boostrap IL publishing in the beacon node --- beacon_node/http_api/src/lib.rs | 24 ++++++- beacon_node/http_api/src/validator/mod.rs | 80 ++++++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 630e9d92118..88f478bb92a 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1881,6 +1881,26 @@ pub async fn serve( }, ); + /* + * beacon inclusion lists + */ + + // POST validator/inclusion_list (JSON) + let post_validator_inclusion_list = post_validator_inclusion_list( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + + // POST validator/inclusion_list (SSZ) + let post_validator_inclusion_list_ssz = post_validator_inclusion_list_ssz( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + // POST beacon/rewards/sync_committee/{block_id} let post_beacon_rewards_sync_committee = beacon_rewards_path .clone() @@ -3464,7 +3484,8 @@ pub async fn serve( .uor(post_beacon_execution_payload_envelopes_ssz) .uor(post_beacon_execution_payload_bids_ssz) .uor(post_beacon_pool_payload_attestations_ssz) - .uor(post_validator_proposer_preferences_ssz), + .uor(post_validator_proposer_preferences_ssz) + .uor(post_validator_inclusion_list_ssz), ) .uor(post_beacon_blocks) .uor(post_beacon_blinded_blocks) @@ -3478,6 +3499,7 @@ pub async fn serve( .uor(post_beacon_pool_payload_attestations) .uor(post_beacon_pool_bls_to_execution_changes) .uor(post_validator_proposer_preferences) + .uor(post_validator_inclusion_list) .uor(post_beacon_execution_payload_envelopes) .uor(post_beacon_execution_payload_bids) .uor(post_beacon_state_validators) diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 7b904260b8e..7ea6b966eaf 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -30,9 +30,9 @@ use tokio::sync::mpsc::{Sender, UnboundedSender}; use tokio::sync::oneshot; use tracing::{debug, error, info, warn}; use types::{ - BeaconState, Epoch, EthSpec, ForkName, ProposerPreparationData, SignedAggregateAndProof, - SignedContributionAndProof, SignedProposerPreferences, SignedValidatorRegistrationData, Slot, - SyncContributionData, ValidatorSubscription, + BeaconState, Epoch, EthSpec, ForkName, InclusionList, ProposerPreparationData, + SignedAggregateAndProof, SignedContributionAndProof, SignedProposerPreferences, + SignedValidatorRegistrationData, Slot, SyncContributionData, ValidatorSubscription, }; use warp::{Filter, Rejection, Reply, http::response::Builder}; use warp_utils::reject::convert_rejection; @@ -1297,3 +1297,77 @@ fn publish_proposer_preferences( )) } } + +/// POST validator/inclusion_list (JSON) +pub fn post_validator_inclusion_list( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("inclusion_list")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(warp::header::(CONSENSUS_VERSION_HEADER)) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |inclusion_list: InclusionList, + _fork_name: ForkName, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_response_task(Priority::P0, move || { + publish_inclusion_list(&chain, &network_tx, inclusion_list)?; + Ok(warp::reply()) + }) + }, + ) + .boxed() +} + +/// POST validator/proposer_preferences (SSZ) +pub fn post_validator_inclusion_list_ssz( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("inclusion_list")) + .and(warp::path::end()) + .and(warp::body::bytes()) + .and(warp::header::(CONSENSUS_VERSION_HEADER)) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |body_bytes: Bytes, + _fork_name: ForkName, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_response_task(Priority::P0, move || { + let inclusion_list = + InclusionList::from_ssz_bytes(&body_bytes).map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_inclusion_list(&chain, &network_tx, inclusion_list)?; + Ok(warp::reply()) + }) + }, + ) + .boxed() +} + +fn publish_inclusion_list( + _chain: &BeaconChain, + _network_tx: &UnboundedSender>, + _inclusion_list: InclusionList, +) -> Result<(), warp::Rejection> { + unimplemented!("Function not yet implemented"); +} From 8772d7c34496c45c258bb255e1191935574ee03b Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 19 Aug 2026 14:18:42 +0300 Subject: [PATCH 02/11] Scaffold gossip verification of inclusion lists --- .../gossip_verified_inclusion_list.rs | 68 +++++++++++++++++++ .../src/inclusion_list_verification/mod.rs | 35 ++++++++++ beacon_node/beacon_chain/src/lib.rs | 1 + 3 files changed, 104 insertions(+) create mode 100644 beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs create mode 100644 beacon_node/beacon_chain/src/inclusion_list_verification/mod.rs diff --git a/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs b/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs new file mode 100644 index 00000000000..b5166ece8d7 --- /dev/null +++ b/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs @@ -0,0 +1,68 @@ +use crate::inclusion_list_verification::InclusionListVerificationError; +use crate::{BeaconChain, BeaconChainTypes}; +use std::sync::Arc; +use tracing::debug; +use types::{ChainSpec, SignedInclusionList}; + +pub struct GossipVerifiedInclusionListContext<'a, T: BeaconChainTypes> { + // TODO(heze): complete while implementing the gossip verification of inclusion lists + pub slot_clock: &'a T::SlotClock, + pub spec: &'a ChainSpec, +} + +pub struct GossipVerifiedInclusionList { + pub signed_inclusion_list: Arc, +} + +impl GossipVerifiedInclusionList { + pub fn new( + signed_inclusion_list: Arc, + _ctx: &GossipVerifiedInclusionListContext<'_, T>, + ) -> Result { + // TODO(heze): implement gossip verification for inclusion lists + Ok(Self { + signed_inclusion_list, + }) + } +} + +impl BeaconChain { + pub fn inclusion_list_verification_context(&self) -> GossipVerifiedInclusionListContext<'_, T> { + GossipVerifiedInclusionListContext { + slot_clock: &self.slot_clock, + spec: &self.spec, + } + } + + pub fn verify_inclusion_list_for_gossip( + &self, + signed_inclusion_list: Arc, + ) -> Result { + let slot = signed_inclusion_list.message.slot; + let validator_index = signed_inclusion_list.message.validator_index; + + let ctx = self.inclusion_list_verification_context(); + match GossipVerifiedInclusionList::new(signed_inclusion_list, &ctx) { + Ok(verified) => { + debug!( + %slot, + %validator_index, + "Successfully verified gossip inclusion list" + ); + + // TODO(heze): emit the inclusion_list SSE event + + Ok(verified) + } + Err(e) => { + debug!( + error = e.to_string(), + %slot, + %validator_index, + "Rejected gossip inclusion list" + ); + Err(e) + } + } + } +} diff --git a/beacon_node/beacon_chain/src/inclusion_list_verification/mod.rs b/beacon_node/beacon_chain/src/inclusion_list_verification/mod.rs new file mode 100644 index 00000000000..2842c901464 --- /dev/null +++ b/beacon_node/beacon_chain/src/inclusion_list_verification/mod.rs @@ -0,0 +1,35 @@ +use crate::BeaconChainError; +use std::sync::Arc; +use types::{BeaconStateError, Slot}; + +pub mod gossip_verified_inclusion_list; + +#[derive(Debug)] +pub enum InclusionListVerificationError { + // Two valid inclusion lists were already seen from this validator for this slot. + AlreadySeenTwice { validator_index: u64, slot: Slot }, + // The slot clock cannot read. + UnableToReadSlot, + // Beacon Chain error + BeaconChainError(Arc), + // Beacon State error + BeaconStateError(BeaconStateError), +} + +impl std::fmt::Display for InclusionListVerificationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From for InclusionListVerificationError { + fn from(e: BeaconChainError) -> Self { + InclusionListVerificationError::BeaconChainError(Arc::new(e)) + } +} + +impl From for InclusionListVerificationError { + fn from(e: BeaconStateError) -> Self { + InclusionListVerificationError::BeaconStateError(e) + } +} diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index b4e71f07573..9ee1f424281 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -30,6 +30,7 @@ pub mod fork_choice_signal; pub mod graffiti_calculator; pub mod historical_blocks; pub mod historical_data_columns; +pub mod inclusion_list_verification; pub mod invariants; pub mod kzg_utils; pub mod light_client_finality_update_verification; From 0608094a9d6717db71fa2f331d914afc102bf3c5 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 19 Aug 2026 14:20:09 +0300 Subject: [PATCH 03/11] Wire gossip verification of inclusion lists into the il publishing handler --- beacon_node/http_api/src/validator/mod.rs | 66 +++++++++++++++++++---- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 7ea6b966eaf..eb7fb76a2c5 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -9,6 +9,7 @@ use crate::utils::{ use crate::version::{V1, V2, V3, V4, add_ssz_content_type_header, unsupported_version_rejection}; use crate::{StateId, attester_duties, proposer_duties, ptc_duties, sync_committees}; use beacon_chain::attestation_verification::VerifiedAttestation; +use beacon_chain::inclusion_list_verification::InclusionListVerificationError; use beacon_chain::proposer_preferences_verification::ProposerPreferencesError; use beacon_chain::{AttestationError, BeaconChain, BeaconChainError, BeaconChainTypes}; use bls::PublicKeyBytes; @@ -30,8 +31,8 @@ use tokio::sync::mpsc::{Sender, UnboundedSender}; use tokio::sync::oneshot; use tracing::{debug, error, info, warn}; use types::{ - BeaconState, Epoch, EthSpec, ForkName, InclusionList, ProposerPreparationData, - SignedAggregateAndProof, SignedContributionAndProof, SignedProposerPreferences, + BeaconState, Epoch, EthSpec, ForkName, ProposerPreparationData, SignedAggregateAndProof, + SignedContributionAndProof, SignedInclusionList, SignedProposerPreferences, SignedValidatorRegistrationData, Slot, SyncContributionData, ValidatorSubscription, }; use warp::{Filter, Rejection, Reply, http::response::Builder}; @@ -1315,13 +1316,13 @@ pub fn post_validator_inclusion_list( .and(chain_filter) .and(network_tx_filter) .then( - |inclusion_list: InclusionList, + |signed_inclusion_list: SignedInclusionList, _fork_name: ForkName, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { - publish_inclusion_list(&chain, &network_tx, inclusion_list)?; + publish_inclusion_list(&chain, &network_tx, signed_inclusion_list)?; Ok(warp::reply()) }) }, @@ -1352,11 +1353,11 @@ pub fn post_validator_inclusion_list_ssz( chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { - let inclusion_list = - InclusionList::from_ssz_bytes(&body_bytes).map_err(|e| { + let signed_inclusion_list = SignedInclusionList::from_ssz_bytes(&body_bytes) + .map_err(|e| { warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) })?; - publish_inclusion_list(&chain, &network_tx, inclusion_list)?; + publish_inclusion_list(&chain, &network_tx, signed_inclusion_list)?; Ok(warp::reply()) }) }, @@ -1365,9 +1366,54 @@ pub fn post_validator_inclusion_list_ssz( } fn publish_inclusion_list( - _chain: &BeaconChain, + chain: &BeaconChain, _network_tx: &UnboundedSender>, - _inclusion_list: InclusionList, + signed_inclusion_list: SignedInclusionList, ) -> Result<(), warp::Rejection> { - unimplemented!("Function not yet implemented"); + if !chain.spec.is_heze_scheduled() { + return Err(warp_utils::reject::custom_bad_request( + "Inclusion lists publishing is not supported before the Heze fork".into(), + )); + } + + let slot = signed_inclusion_list.message.slot; + let validator_index = signed_inclusion_list.message.validator_index; + + match chain.verify_inclusion_list_for_gossip(Arc::new(signed_inclusion_list)) { + Ok(_verified_inclusion_list) => { + // inclusion list is verified, so we can publish it to the network + Ok(()) + } + Err(InclusionListVerificationError::AlreadySeenTwice { .. }) => { + debug!( + %slot, + %validator_index, + "Two valid inclusion lists were already seen" + ); + Ok(()) + } + Err( + e @ (InclusionListVerificationError::BeaconChainError(_) + | InclusionListVerificationError::BeaconStateError(_) + | InclusionListVerificationError::UnableToReadSlot), + ) => { + error!(%slot, error = ?e, "Internal error verifying inclusion list"); + Err(warp_utils::reject::custom_server_error(format!( + "internal error verifying inclusion list: {e:?}" + ))) + } + // TODO(heze): remove once the the IL gossip verification errors are added to InclusionListVerificationError + #[allow(unreachable_patterns)] + Err(e) => { + warn!( + %slot, + %validator_index, + error = ?e, + "Unable to process sync subscriptions" + ); + Err(warp_utils::reject::custom_bad_request(format!( + "Error publishing inclusion list: {e}" + ))) + } + } } From c7585bdd5023fae3e44b70461f3a3b9b3e91faad Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 19 Aug 2026 15:19:11 +0300 Subject: [PATCH 04/11] Ensure endpoint throws before heze and while syncing --- beacon_node/http_api/src/lib.rs | 2 ++ beacon_node/http_api/src/validator/mod.rs | 32 ++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 88f478bb92a..fd33391a298 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1888,6 +1888,7 @@ pub async fn serve( // POST validator/inclusion_list (JSON) let post_validator_inclusion_list = post_validator_inclusion_list( eth_v1.clone(), + not_while_syncing_filter.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), @@ -1896,6 +1897,7 @@ pub async fn serve( // POST validator/inclusion_list (SSZ) let post_validator_inclusion_list_ssz = post_validator_inclusion_list_ssz( eth_v1.clone(), + not_while_syncing_filter.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index eb7fb76a2c5..0100e731b4c 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -40,6 +40,15 @@ use warp_utils::reject::convert_rejection; pub mod execution_payload_envelopes; +fn ensure_heze_consensus_version(fork_name: ForkName) -> Result<(), Rejection> { + if !fork_name.gloas_enabled() { + return Err(warp_utils::reject::custom_bad_request(format!( + "Eth-Consensus-Version {fork_name} is not supported for execution payload envelopes" + ))); + } + Ok(()) +} + /// 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( @@ -1302,6 +1311,7 @@ fn publish_proposer_preferences( /// POST validator/inclusion_list (JSON) pub fn post_validator_inclusion_list( eth_v1: EthV1Filter, + not_while_syncing_filter: NotWhileSyncingFilter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, network_tx_filter: NetworkTxFilter, @@ -1312,16 +1322,20 @@ pub fn post_validator_inclusion_list( .and(warp::path::end()) .and(warp_utils::json::json()) .and(warp::header::(CONSENSUS_VERSION_HEADER)) + .and(not_while_syncing_filter.clone()) .and(task_spawner_filter) .and(chain_filter) .and(network_tx_filter) .then( |signed_inclusion_list: SignedInclusionList, - _fork_name: ForkName, + fork_name: ForkName, + not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { + not_synced_filter?; + ensure_heze_consensus_version(fork_name)?; publish_inclusion_list(&chain, &network_tx, signed_inclusion_list)?; Ok(warp::reply()) }) @@ -1333,6 +1347,7 @@ pub fn post_validator_inclusion_list( /// POST validator/proposer_preferences (SSZ) pub fn post_validator_inclusion_list_ssz( eth_v1: EthV1Filter, + not_while_syncing_filter: NotWhileSyncingFilter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, network_tx_filter: NetworkTxFilter, @@ -1343,16 +1358,20 @@ pub fn post_validator_inclusion_list_ssz( .and(warp::path::end()) .and(warp::body::bytes()) .and(warp::header::(CONSENSUS_VERSION_HEADER)) + .and(not_while_syncing_filter) .and(task_spawner_filter) .and(chain_filter) .and(network_tx_filter) .then( |body_bytes: Bytes, - _fork_name: ForkName, + fork_name: ForkName, + not_while_syncing_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { + not_while_syncing_filter?; + ensure_heze_consensus_version(fork_name)?; let signed_inclusion_list = SignedInclusionList::from_ssz_bytes(&body_bytes) .map_err(|e| { warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) @@ -1370,15 +1389,16 @@ fn publish_inclusion_list( _network_tx: &UnboundedSender>, signed_inclusion_list: SignedInclusionList, ) -> Result<(), warp::Rejection> { - if !chain.spec.is_heze_scheduled() { + let slot = signed_inclusion_list.message.slot; + let validator_index = signed_inclusion_list.message.validator_index; + let fork_name = chain.spec.fork_name_at_slot::(slot); + + if !fork_name.heze_enabled() { return Err(warp_utils::reject::custom_bad_request( "Inclusion lists publishing is not supported before the Heze fork".into(), )); } - let slot = signed_inclusion_list.message.slot; - let validator_index = signed_inclusion_list.message.validator_index; - match chain.verify_inclusion_list_for_gossip(Arc::new(signed_inclusion_list)) { Ok(_verified_inclusion_list) => { // inclusion list is verified, so we can publish it to the network From b4cb8d6106f7c7a187e1c0404e82e3f7e49f99b6 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 19 Aug 2026 15:40:41 +0300 Subject: [PATCH 05/11] Publish inclusion list pubsub message if verification succeeded --- beacon_node/http_api/src/validator/mod.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 0100e731b4c..3052d4bf4ea 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1344,7 +1344,7 @@ pub fn post_validator_inclusion_list( .boxed() } -/// POST validator/proposer_preferences (SSZ) +/// POST validator/inclusion_list (SSZ) pub fn post_validator_inclusion_list_ssz( eth_v1: EthV1Filter, not_while_syncing_filter: NotWhileSyncingFilter, @@ -1386,7 +1386,7 @@ pub fn post_validator_inclusion_list_ssz( fn publish_inclusion_list( chain: &BeaconChain, - _network_tx: &UnboundedSender>, + network_tx: &UnboundedSender>, signed_inclusion_list: SignedInclusionList, ) -> Result<(), warp::Rejection> { let slot = signed_inclusion_list.message.slot; @@ -1400,8 +1400,13 @@ fn publish_inclusion_list( } match chain.verify_inclusion_list_for_gossip(Arc::new(signed_inclusion_list)) { - Ok(_verified_inclusion_list) => { - // inclusion list is verified, so we can publish it to the network + Ok(verified_inclusion_list) => { + crate::utils::publish_pubsub_message( + network_tx, + PubsubMessage::InclusionList(Box::new( + (*verified_inclusion_list.signed_inclusion_list).clone(), + )), + )?; Ok(()) } Err(InclusionListVerificationError::AlreadySeenTwice { .. }) => { @@ -1422,17 +1427,17 @@ fn publish_inclusion_list( "internal error verifying inclusion list: {e:?}" ))) } - // TODO(heze): remove once the the IL gossip verification errors are added to InclusionListVerificationError + // TODO(heze): remove once the IL gossip verification errors are added to InclusionListVerificationError #[allow(unreachable_patterns)] Err(e) => { warn!( %slot, %validator_index, error = ?e, - "Unable to process sync subscriptions" + "Inclusion list failed gossip verification" ); Err(warp_utils::reject::custom_bad_request(format!( - "Error publishing inclusion list: {e}" + "inclusion list failed gossip verification: {e}" ))) } } From 663fdb1479d429ca4a8279d43bd9efc7fde990a4 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 19 Aug 2026 15:41:02 +0300 Subject: [PATCH 06/11] Update naming and add more cleanup --- .../gossip_verified_inclusion_list.rs | 12 +++---- .../src/inclusion_list_verification/mod.rs | 8 ++--- beacon_node/http_api/src/validator/mod.rs | 32 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs b/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs index b5166ece8d7..746729e1440 100644 --- a/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs +++ b/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use tracing::debug; use types::{ChainSpec, SignedInclusionList}; -pub struct GossipVerifiedInclusionListContext<'a, T: BeaconChainTypes> { +pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { // TODO(heze): complete while implementing the gossip verification of inclusion lists pub slot_clock: &'a T::SlotClock, pub spec: &'a ChainSpec, @@ -17,7 +17,7 @@ pub struct GossipVerifiedInclusionList { impl GossipVerifiedInclusionList { pub fn new( signed_inclusion_list: Arc, - _ctx: &GossipVerifiedInclusionListContext<'_, T>, + _ctx: &GossipVerificationContext<'_, T>, ) -> Result { // TODO(heze): implement gossip verification for inclusion lists Ok(Self { @@ -27,8 +27,8 @@ impl GossipVerifiedInclusionList { } impl BeaconChain { - pub fn inclusion_list_verification_context(&self) -> GossipVerifiedInclusionListContext<'_, T> { - GossipVerifiedInclusionListContext { + pub fn inclusion_list_gossip_verification_context(&self) -> GossipVerificationContext<'_, T> { + GossipVerificationContext { slot_clock: &self.slot_clock, spec: &self.spec, } @@ -41,7 +41,7 @@ impl BeaconChain { let slot = signed_inclusion_list.message.slot; let validator_index = signed_inclusion_list.message.validator_index; - let ctx = self.inclusion_list_verification_context(); + let ctx = self.inclusion_list_gossip_verification_context(); match GossipVerifiedInclusionList::new(signed_inclusion_list, &ctx) { Ok(verified) => { debug!( @@ -56,7 +56,7 @@ impl BeaconChain { } Err(e) => { debug!( - error = e.to_string(), + error = ?e, %slot, %validator_index, "Rejected gossip inclusion list" diff --git a/beacon_node/beacon_chain/src/inclusion_list_verification/mod.rs b/beacon_node/beacon_chain/src/inclusion_list_verification/mod.rs index 2842c901464..3cb1a3dcd95 100644 --- a/beacon_node/beacon_chain/src/inclusion_list_verification/mod.rs +++ b/beacon_node/beacon_chain/src/inclusion_list_verification/mod.rs @@ -6,13 +6,13 @@ pub mod gossip_verified_inclusion_list; #[derive(Debug)] pub enum InclusionListVerificationError { - // Two valid inclusion lists were already seen from this validator for this slot. + /// Two valid inclusion lists were already seen from this validator for this slot. AlreadySeenTwice { validator_index: u64, slot: Slot }, - // The slot clock cannot read. + /// The slot clock cannot read. UnableToReadSlot, - // Beacon Chain error + /// Beacon Chain error BeaconChainError(Arc), - // Beacon State error + /// Beacon State error BeaconStateError(BeaconStateError), } diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 3052d4bf4ea..164ef65c417 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -40,15 +40,6 @@ use warp_utils::reject::convert_rejection; pub mod execution_payload_envelopes; -fn ensure_heze_consensus_version(fork_name: ForkName) -> Result<(), Rejection> { - if !fork_name.gloas_enabled() { - return Err(warp_utils::reject::custom_bad_request(format!( - "Eth-Consensus-Version {fork_name} is not supported for execution payload envelopes" - ))); - } - Ok(()) -} - /// 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( @@ -81,7 +72,7 @@ pub fn get_validator_sync_committee_contribution( .and(warp::path("sync_committee_contribution")) .and(warp::path::end()) .and(warp::query::()) - .and(not_while_syncing_filter.clone()) + .and(not_while_syncing_filter) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .then( @@ -128,7 +119,7 @@ pub fn post_validator_duties_sync( )) })) .and(warp::path::end()) - .and(not_while_syncing_filter.clone()) + .and(not_while_syncing_filter) .and(warp_utils::json::json()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) @@ -1365,12 +1356,12 @@ pub fn post_validator_inclusion_list_ssz( .then( |body_bytes: Bytes, fork_name: ForkName, - not_while_syncing_filter: Result<(), Rejection>, + not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { - not_while_syncing_filter?; + not_synced_filter?; ensure_heze_consensus_version(fork_name)?; let signed_inclusion_list = SignedInclusionList::from_ssz_bytes(&body_bytes) .map_err(|e| { @@ -1384,6 +1375,15 @@ pub fn post_validator_inclusion_list_ssz( .boxed() } +fn ensure_heze_consensus_version(fork_name: ForkName) -> Result<(), Rejection> { + if !fork_name.heze_enabled() { + return Err(warp_utils::reject::custom_bad_request(format!( + "Eth-Consensus-Version {fork_name} is not supported for inclusion lists" + ))); + } + Ok(()) +} + fn publish_inclusion_list( chain: &BeaconChain, network_tx: &UnboundedSender>, @@ -1411,8 +1411,8 @@ fn publish_inclusion_list( } Err(InclusionListVerificationError::AlreadySeenTwice { .. }) => { debug!( - %slot, - %validator_index, + %slot, + %validator_index, "Two valid inclusion lists were already seen" ); Ok(()) @@ -1424,7 +1424,7 @@ fn publish_inclusion_list( ) => { error!(%slot, error = ?e, "Internal error verifying inclusion list"); Err(warp_utils::reject::custom_server_error(format!( - "internal error verifying inclusion list: {e:?}" + "internal error verifying inclusion list: {e}" ))) } // TODO(heze): remove once the IL gossip verification errors are added to InclusionListVerificationError From c61503bf1cc09577c24ecd90899ca1bc12670eed Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 19 Aug 2026 23:06:35 +0300 Subject: [PATCH 07/11] Tests: add incorrect fork name tests --- beacon_node/http_api/tests/tests.rs | 159 +++++++++++++++++++++++++++- common/eth2/src/lib.rs | 42 ++++++++ 2 files changed, 200 insertions(+), 1 deletion(-) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index ae54f110140..09d3cf6be9b 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -8315,7 +8315,7 @@ impl ApiTester { let trusted_peers = self.ctx.network_globals.as_ref().unwrap().trusted_peers(); // Check that there aren't any trusted peers on startup assert!(trusted_peers.is_empty()); - let enr = AdminPeer {enr: "enr:-QESuEDpVVjo8dmDuneRhLnXdIGY3e9NQiaG4sJR3GS-VMQCQDsmBYoQhJRaPeZzPlTsZj2F8v-iV4lKJEYIRIyztqexHodhdHRuZXRziAwAAAAAAAAAhmNsaWVudNiKTGlnaHRob3VzZYw3LjAuMC1iZXRhLjSEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhIe11XmDaXA2kCoBBPkAOitZAAAAAAAAAAKEcXVpY4IjKYVxdWljNoIjg4lzZWNwMjU2azGhA43ihEr9BUVVnIHIfFqBR3Izs4YRHHPsTqIbUgEb3Hc8iHN5bmNuZXRzD4N0Y3CCIyiEdGNwNoIjgoN1ZHCCIyiEdWRwNoIjgg".to_string()}; + let enr = AdminPeer { enr: "enr:-QESuEDpVVjo8dmDuneRhLnXdIGY3e9NQiaG4sJR3GS-VMQCQDsmBYoQhJRaPeZzPlTsZj2F8v-iV4lKJEYIRIyztqexHodhdHRuZXRziAwAAAAAAAAAhmNsaWVudNiKTGlnaHRob3VzZYw3LjAuMC1iZXRhLjSEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhIe11XmDaXA2kCoBBPkAOitZAAAAAAAAAAKEcXVpY4IjKYVxdWljNoIjg4lzZWNwMjU2azGhA43ihEr9BUVVnIHIfFqBR3Izs4YRHHPsTqIbUgEb3Hc8iHN5bmNuZXRzD4N0Y3CCIyiEdGNwNoIjgoN1ZHCCIyiEdWRwNoIjgg".to_string() }; self.client .post_lighthouse_add_peer(enr.clone()) .await @@ -9046,6 +9046,125 @@ impl ApiTester { assert_eq!(result.execution_optimistic, Some(true)); } + pub async fn test_inclusion_list_post_fork_name_invalid_returns_400(mut self) -> Self { + if !self.chain.spec.is_heze_scheduled() { + return self; + } + + let epoch = self.chain.epoch().unwrap(); + let slot = self.chain.slot().unwrap(); + let genesis_validators_root = self.chain.genesis_validators_root; + let head_state = self.chain.head_beacon_state_cloned(); + let dependent_root = self + .chain + .block_root_at_slot( + (epoch - 1).start_slot(E::slots_per_epoch()) - 1, + WhenSlotSkipped::Prev, + ) + .unwrap() + .unwrap_or(self.chain.head_beacon_block_root()); + // TODO: use get_inclusion_list_committee from the beacon state when available + let beacon_committee = head_state.get_beacon_committees_at_slot(slot).unwrap(); + let validator_index = beacon_committee[0].committee[0] as u64; + let sk: &SecretKey = &self.validator_keypairs()[validator_index as usize].sk; + let inclusion_list = InclusionList { + slot, + validator_index, + dependent_root, + transactions: ProgressiveTransactions::new(Vec::new()), + }; + + let signed_il = self.sign_inclusion_list( + inclusion_list, + sk, + epoch, + &head_state.fork(), + genesis_validators_root, + ); + + let err = self + .client + .post_validator_inclusion_list(&signed_il, ForkName::Gloas) + .await + .expect_err("publishing inclusion list should fail"); + + assert_eq!(err.status(), Some(StatusCode::BAD_REQUEST)); + assert!(self.network_rx.network_recv.recv().now_or_never().is_none()); + + self + } + + pub async fn test_inclusion_list_post_ssz_fork_name_invalid_returns_400(mut self) -> Self { + if !self.chain.spec.is_heze_scheduled() { + return self; + } + + let epoch = self.chain.epoch().unwrap(); + let slot = self.chain.slot().unwrap(); + let genesis_validators_root = self.chain.genesis_validators_root; + let head_state = self.chain.head_beacon_state_cloned(); + let dependent_root = self + .chain + .block_root_at_slot( + (epoch - 1).start_slot(E::slots_per_epoch()) - 1, + WhenSlotSkipped::Prev, + ) + .unwrap() + .unwrap_or(self.chain.head_beacon_block_root()); + // TODO: use get_inclusion_list_committee from the beacon state when available + let beacon_committee = head_state.get_beacon_committees_at_slot(slot).unwrap(); + let validator_index = beacon_committee[0].committee[0] as u64; + let sk: &SecretKey = &self.validator_keypairs()[validator_index as usize].sk; + let inclusion_list = InclusionList { + slot, + validator_index, + dependent_root, + transactions: ProgressiveTransactions::new(Vec::new()), + }; + + let signed_il = self.sign_inclusion_list( + inclusion_list, + sk, + epoch, + &head_state.fork(), + genesis_validators_root, + ); + + let err = self + .client + .post_validator_inclusion_list_ssz(&signed_il, ForkName::Gloas) + .await + .expect_err("publishing inclusion list should fail"); + + assert_eq!(err.status(), Some(StatusCode::BAD_REQUEST)); + assert!(self.network_rx.network_recv.recv().now_or_never().is_none()); + + self + } + + fn sign_inclusion_list( + &self, + inclusion_list: InclusionList, + sk: &SecretKey, + epoch: Epoch, + fork: &Fork, + genesis_validators_root: Hash256, + ) -> SignedInclusionList { + let domain = self.chain.spec.get_domain( + epoch, + Domain::InclusionListCommittee, + fork, + genesis_validators_root, + ); + let signing_root = inclusion_list.signing_root(domain); + let signature = sk.sign(signing_root); + + SignedInclusionList { + message: inclusion_list, + signature, + } + } + async fn test_get_beacon_rewards_blocks_at_head( &self, ) -> ExecutionOptimisticFinalizedResponse { @@ -10919,3 +11038,41 @@ async fn post_beacon_execution_payload_bids() { .test_post_beacon_execution_payload_bids_ssz() .await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn inclusion_list_publish_pre_heze() { + if fork_name_from_env().is_some_and(|f| f.heze_enabled()) { + return; + } + + /* ApiTester::new_with_hard_forks() + .await + .test_inclusion_list_post_pre_heze_returns_400() + .await + .test_inclusion_list_post_ssz_pre_heze_returns_400()*/ +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn inclusion_list_api() { + if !fork_name_from_env().is_some_and(|f| f.heze_enabled()) { + return; + } + + ApiTester::new_with_hard_forks() + .await + .test_inclusion_list_post_fork_name_invalid_returns_400() + .await + .test_inclusion_list_post_ssz_fork_name_invalid_returns_400() + .await; + + /* + .test_inclusion_list_post_while_syincing_returns_503() + .await + .test_inclusion_list_post_ssz_while_syncing_returns_503() + .await + .test_inclusion_list_post_valid() + .await + .test_inclusion_list_post_ssz_valid() + .await + .test_inclusion_list_post_ssz_malformed_returns_400();*/ +} diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index a48374a8538..843f4cb81a6 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -3372,6 +3372,48 @@ impl BeaconNodeHttpClient { AttestationData::from_ssz_bytes(&response_bytes).map_err(Error::InvalidSsz) } + /// `POST validator/inclusion_list` + pub async fn post_validator_inclusion_list( + &self, + signed_inclusion_list: &SignedInclusionList, + fork_name: ForkName, + ) -> 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"); + + // The request body is wrapped in a `data` object, per the beacon-APIs specs + let body = serde_json::json!({ "data": signed_inclusion_list }); + self.post_generic_with_consensus_version(path, &body, None, fork_name) + .await?; + + Ok(()) + } + + /// `POST validator/inclusion_list` (SSZ) + pub async fn post_validator_inclusion_list_ssz( + &self, + signed_inclusion_list: &SignedInclusionList, + fork_name: ForkName, + ) -> 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"); + + let ssz_body = signed_inclusion_list.as_ssz_bytes(); + + self.post_generic_with_consensus_version_and_ssz_body(path, ssz_body, None, fork_name) + .await?; + + Ok(()) + } + /// `GET validator/payload_attestation_data/{slot}` /// Returns `None` if no block has been received for the requested slot (404). pub async fn get_validator_payload_attestation_data( From 5fd1a6ec291c3ee4f07170a851b17e0dfdea9048 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 20 Aug 2026 12:53:08 +0300 Subject: [PATCH 08/11] Tests: add remaining Heze and pre-Heze endpoint tests --- beacon_node/http_api/src/validator/mod.rs | 4 +- beacon_node/http_api/tests/tests.rs | 269 ++++++++++++++++------ 2 files changed, 199 insertions(+), 74 deletions(-) diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 164ef65c417..8329712c9c1 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1318,7 +1318,7 @@ pub fn post_validator_inclusion_list( .and(chain_filter) .and(network_tx_filter) .then( - |signed_inclusion_list: SignedInclusionList, + |request_body: GenericResponse, fork_name: ForkName, not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, @@ -1327,7 +1327,7 @@ pub fn post_validator_inclusion_list( task_spawner.blocking_response_task(Priority::P0, move || { not_synced_filter?; ensure_heze_consensus_version(fork_name)?; - publish_inclusion_list(&chain, &network_tx, signed_inclusion_list)?; + publish_inclusion_list(&chain, &network_tx, request_body.data)?; Ok(warp::reply()) }) }, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 09d3cf6be9b..d03d8783fca 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -9046,13 +9046,8 @@ impl ApiTester { assert_eq!(result.execution_optimistic, Some(true)); } - pub async fn test_inclusion_list_post_fork_name_invalid_returns_400(mut self) -> Self { - if !self.chain.spec.is_heze_scheduled() { - return self; - } - + fn make_signed_inclusion_list(&self, slot: Slot) -> SignedInclusionList { let epoch = self.chain.epoch().unwrap(); - let slot = self.chain.slot().unwrap(); let genesis_validators_root = self.chain.genesis_validators_root; let head_state = self.chain.head_beacon_state_cloned(); let dependent_root = self @@ -9074,14 +9069,80 @@ impl ApiTester { transactions: ProgressiveTransactions::new(Vec::new()), }; - let signed_il = self.sign_inclusion_list( + self.sign_inclusion_list( inclusion_list, sk, epoch, &head_state.fork(), genesis_validators_root, + ) + } + + fn sign_inclusion_list( + &self, + inclusion_list: InclusionList, + sk: &SecretKey, + epoch: Epoch, + fork: &Fork, + genesis_validators_root: Hash256, + ) -> SignedInclusionList { + let domain = self.chain.spec.get_domain( + epoch, + Domain::InclusionListCommittee, + fork, + genesis_validators_root, ); + let signing_root = inclusion_list.signing_root(domain); + let signature = sk.sign(signing_root); + + SignedInclusionList { + message: inclusion_list, + signature, + } + } + + pub async fn test_inclusion_list_post_pre_heze_returns_400(mut self) -> Self { + let slot = self.chain.slot().unwrap(); + let signed_il = self.make_signed_inclusion_list(slot); + + let response = self + .client + .post_validator_inclusion_list(&signed_il, self.chain.spec.fork_name_at_slot::(slot)) + .await + .expect_err("publishing inclusion list pre-Heze should fail"); + + assert_eq!(response.status(), Some(StatusCode::BAD_REQUEST)); + assert!(self.network_rx.network_recv.recv().now_or_never().is_none()); + + self + } + pub async fn test_inclusion_list_post_ssz_pre_heze_returns_400(mut self) -> Self { + let slot = self.chain.slot().unwrap(); + let signed_il = self.make_signed_inclusion_list(slot); + + let response = self + .client + .post_validator_inclusion_list_ssz( + &signed_il, + self.chain.spec.fork_name_at_slot::(slot), + ) + .await + .expect_err("publishing inclusion list pre-Heze should fail"); + + assert_eq!(response.status(), Some(StatusCode::BAD_REQUEST)); + assert!(self.network_rx.network_recv.recv().now_or_never().is_none()); + + self + } + + pub async fn test_inclusion_list_post_fork_name_invalid_returns_400(mut self) -> Self { + if !self.chain.spec.is_heze_scheduled() { + return self; + } + + let slot = self.chain.slot().unwrap(); + let signed_il = self.make_signed_inclusion_list(slot); let err = self .client .post_validator_inclusion_list(&signed_il, ForkName::Gloas) @@ -9099,70 +9160,137 @@ impl ApiTester { return self; } - let epoch = self.chain.epoch().unwrap(); let slot = self.chain.slot().unwrap(); - let genesis_validators_root = self.chain.genesis_validators_root; - let head_state = self.chain.head_beacon_state_cloned(); - let dependent_root = self - .chain - .block_root_at_slot( - (epoch - 1).start_slot(E::slots_per_epoch()) - 1, - WhenSlotSkipped::Prev, + let signed_il = self.make_signed_inclusion_list(slot); + let err = self + .client + .post_validator_inclusion_list_ssz(&signed_il, ForkName::Gloas) + .await + .expect_err("publishing inclusion list should fail"); + + assert_eq!(err.status(), Some(StatusCode::BAD_REQUEST)); + assert!(self.network_rx.network_recv.recv().now_or_never().is_none()); + + self + } + + pub async fn test_inclusion_list_post_while_syncing_returns_503(mut self) -> Self { + if !self.chain.spec.is_heze_scheduled() { + return self; + } + + let original_slot = self.chain.slot().unwrap(); + let signed_il = self.make_signed_inclusion_list(original_slot); + + let network_globals = self.ctx.network_globals.as_ref().unwrap(); + *network_globals.sync_state.write() = SyncState::SyncingFinalized { + start_slot: Slot::new(0), + target_slot: Slot::new(u64::MAX), + }; + + let head_slot = self.chain.canonical_head.cached_head().head_slot(); + let tolerance = self.chain.config.sync_tolerance_epochs * E::slots_per_epoch(); + self.chain + .slot_clock + .set_slot(head_slot.as_u64() + tolerance + 1); + + while self.network_rx.network_recv.recv().now_or_never().is_some() {} + + let err = self + .client + .post_validator_inclusion_list( + &signed_il, + self.chain.spec.fork_name_at_slot::(original_slot), ) - .unwrap() - .unwrap_or(self.chain.head_beacon_block_root()); - // TODO: use get_inclusion_list_committee from the beacon state when available - let beacon_committee = head_state.get_beacon_committees_at_slot(slot).unwrap(); - let validator_index = beacon_committee[0].committee[0] as u64; - let sk: &SecretKey = &self.validator_keypairs()[validator_index as usize].sk; - let inclusion_list = InclusionList { - slot, - validator_index, - dependent_root, - transactions: ProgressiveTransactions::new(Vec::new()), + .await + .expect_err("publishing inclusion list should fail while syncing"); + + assert_eq!(err.status(), Some(StatusCode::SERVICE_UNAVAILABLE)); + assert!(self.network_rx.network_recv.recv().now_or_never().is_none()); + + *network_globals.sync_state.write() = SyncState::Synced; + self.chain.slot_clock.set_slot(original_slot.as_u64() + 1); + + self + } + + pub async fn test_inclusion_list_post_ssz_while_syncing_returns_503(mut self) -> Self { + if !self.chain.spec.is_heze_scheduled() { + return self; + } + + let original_slot = self.chain.slot().unwrap(); + let signed_il = self.make_signed_inclusion_list(original_slot); + + let network_globals = self.ctx.network_globals.as_ref().unwrap(); + *network_globals.sync_state.write() = SyncState::SyncingFinalized { + start_slot: Slot::new(0), + target_slot: Slot::new(u64::MAX), }; - let signed_il = self.sign_inclusion_list( - inclusion_list, - sk, - epoch, - &head_state.fork(), - genesis_validators_root, - ); + let head_slot = self.chain.canonical_head.cached_head().head_slot(); + let tolerance = self.chain.config.sync_tolerance_epochs * E::slots_per_epoch(); + self.chain + .slot_clock + .set_slot(head_slot.as_u64() + tolerance + 1); + + while self.network_rx.network_recv.recv().now_or_never().is_some() {} let err = self .client - .post_validator_inclusion_list_ssz(&signed_il, ForkName::Gloas) + .post_validator_inclusion_list_ssz( + &signed_il, + self.chain.spec.fork_name_at_slot::(original_slot), + ) .await - .expect_err("publishing inclusion list should fail"); + .expect_err("publishing inclusion list should fail while syncing"); - assert_eq!(err.status(), Some(StatusCode::BAD_REQUEST)); + assert_eq!(err.status(), Some(StatusCode::SERVICE_UNAVAILABLE)); assert!(self.network_rx.network_recv.recv().now_or_never().is_none()); + *network_globals.sync_state.write() = SyncState::Synced; + self.chain.slot_clock.set_slot(original_slot.as_u64() + 1); + self } - fn sign_inclusion_list( - &self, - inclusion_list: InclusionList, - sk: &SecretKey, - epoch: Epoch, - fork: &Fork, - genesis_validators_root: Hash256, - ) -> SignedInclusionList { - let domain = self.chain.spec.get_domain( - epoch, - Domain::InclusionListCommittee, - fork, - genesis_validators_root, - ); - let signing_root = inclusion_list.signing_root(domain); - let signature = sk.sign(signing_root); + pub async fn test_inclusion_list_post_valid(mut self) -> Self { + if !self.chain.spec.is_heze_scheduled() { + return self; + } - SignedInclusionList { - message: inclusion_list, - signature, + let slot = self.chain.slot().unwrap(); + let signed_il = self.make_signed_inclusion_list(slot); + + self.client + .post_validator_inclusion_list(&signed_il, self.chain.spec.fork_name_at_slot::(slot)) + .await + .expect("publishing valid inclusion list (json) should be successful"); + + assert!(self.network_rx.network_recv.recv().now_or_never().is_some()); + + self + } + + pub async fn test_inclusion_list_post_ssz_valid(mut self) -> Self { + if !self.chain.spec.is_heze_scheduled() { + return self; } + + let slot = self.chain.slot().unwrap(); + let signed_il = self.make_signed_inclusion_list(slot); + + self.client + .post_validator_inclusion_list_ssz( + &signed_il, + self.chain.spec.fork_name_at_slot::(slot), + ) + .await + .expect("publishing valid inclusion list (ssz) should be successful"); + + assert!(self.network_rx.network_recv.recv().now_or_never().is_some()); + + self } async fn test_get_beacon_rewards_blocks_at_head( @@ -11044,12 +11172,12 @@ async fn inclusion_list_publish_pre_heze() { if fork_name_from_env().is_some_and(|f| f.heze_enabled()) { return; } - - /* ApiTester::new_with_hard_forks() - .await - .test_inclusion_list_post_pre_heze_returns_400() - .await - .test_inclusion_list_post_ssz_pre_heze_returns_400()*/ + ApiTester::new_with_hard_forks() + .await + .test_inclusion_list_post_pre_heze_returns_400() + .await + .test_inclusion_list_post_ssz_pre_heze_returns_400() + .await; } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -11063,16 +11191,13 @@ async fn inclusion_list_api() { .test_inclusion_list_post_fork_name_invalid_returns_400() .await .test_inclusion_list_post_ssz_fork_name_invalid_returns_400() + .await + .test_inclusion_list_post_while_syncing_returns_503() + .await + .test_inclusion_list_post_ssz_while_syncing_returns_503() + .await + .test_inclusion_list_post_valid() + .await + .test_inclusion_list_post_ssz_valid() .await; - - /* - .test_inclusion_list_post_while_syincing_returns_503() - .await - .test_inclusion_list_post_ssz_while_syncing_returns_503() - .await - .test_inclusion_list_post_valid() - .await - .test_inclusion_list_post_ssz_valid() - .await - .test_inclusion_list_post_ssz_malformed_returns_400();*/ } From 2218f6713cf4f4adb78669ea19f9648c6a976d57 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 20 Aug 2026 13:49:17 +0300 Subject: [PATCH 09/11] Tests: cleanup --- beacon_node/http_api/tests/tests.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index d03d8783fca..04ecdbea85a 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -9059,8 +9059,8 @@ impl ApiTester { .unwrap() .unwrap_or(self.chain.head_beacon_block_root()); // TODO: use get_inclusion_list_committee from the beacon state when available - let beacon_committee = head_state.get_beacon_committees_at_slot(slot).unwrap(); - let validator_index = beacon_committee[0].committee[0] as u64; + let beacon_committees = head_state.get_beacon_committees_at_slot(slot).unwrap(); + let validator_index = beacon_committees[0].committee[0] as u64; let sk: &SecretKey = &self.validator_keypairs()[validator_index as usize].sk; let inclusion_list = InclusionList { slot, @@ -9072,7 +9072,6 @@ impl ApiTester { self.sign_inclusion_list( inclusion_list, sk, - epoch, &head_state.fork(), genesis_validators_root, ) @@ -9082,10 +9081,10 @@ impl ApiTester { &self, inclusion_list: InclusionList, sk: &SecretKey, - epoch: Epoch, fork: &Fork, genesis_validators_root: Hash256, ) -> SignedInclusionList { + let epoch = inclusion_list.slot.epoch(E::slots_per_epoch()); let domain = self.chain.spec.get_domain( epoch, Domain::InclusionListCommittee, @@ -9267,7 +9266,12 @@ impl ApiTester { .await .expect("publishing valid inclusion list (json) should be successful"); - assert!(self.network_rx.network_recv.recv().now_or_never().is_some()); + assert!( + self.network_rx.network_recv.recv().await.is_some(), + "valid inclusion list should be sent to network" + ); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); self } @@ -9288,7 +9292,12 @@ impl ApiTester { .await .expect("publishing valid inclusion list (ssz) should be successful"); - assert!(self.network_rx.network_recv.recv().now_or_never().is_some()); + assert!( + self.network_rx.network_recv.recv().await.is_some(), + "valid inclusion list (SSZ) should be sent to network" + ); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); self } @@ -11180,6 +11189,7 @@ async fn inclusion_list_publish_pre_heze() { .await; } +// TODO(heze): once IL gossip verification lands, cover the endpoint's error mapping #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn inclusion_list_api() { if !fork_name_from_env().is_some_and(|f| f.heze_enabled()) { From 83db96dd20add4b44cf979c61df71299d2fbaede Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 20 Aug 2026 14:33:17 +0300 Subject: [PATCH 10/11] Cleanup --- beacon_node/http_api/src/lib.rs | 43 ++++++++++++++--------------- beacon_node/http_api/tests/tests.rs | 2 +- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index fd33391a298..599d44ae3c9 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1881,28 +1881,6 @@ pub async fn serve( }, ); - /* - * beacon inclusion lists - */ - - // POST validator/inclusion_list (JSON) - let post_validator_inclusion_list = post_validator_inclusion_list( - eth_v1.clone(), - not_while_syncing_filter.clone(), - task_spawner_filter.clone(), - chain_filter.clone(), - network_tx_filter.clone(), - ); - - // POST validator/inclusion_list (SSZ) - let post_validator_inclusion_list_ssz = post_validator_inclusion_list_ssz( - eth_v1.clone(), - not_while_syncing_filter.clone(), - task_spawner_filter.clone(), - chain_filter.clone(), - network_tx_filter.clone(), - ); - // POST beacon/rewards/sync_committee/{block_id} let post_beacon_rewards_sync_committee = beacon_rewards_path .clone() @@ -1928,6 +1906,27 @@ pub async fn serve( }, ); + /* + * inclusion lists + */ + + // POST validator/inclusion_list (JSON) + let post_validator_inclusion_list = post_validator_inclusion_list( + eth_v1.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + + // POST validator/inclusion_list (SSZ) + let post_validator_inclusion_list_ssz = post_validator_inclusion_list_ssz( + eth_v1.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); /* * config */ diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 04ecdbea85a..ab733962801 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -9058,7 +9058,7 @@ impl ApiTester { ) .unwrap() .unwrap_or(self.chain.head_beacon_block_root()); - // TODO: use get_inclusion_list_committee from the beacon state when available + // TODO(heze): use get_inclusion_list_committee from the beacon state when available let beacon_committees = head_state.get_beacon_committees_at_slot(slot).unwrap(); let validator_index = beacon_committees[0].committee[0] as u64; let sk: &SecretKey = &self.validator_keypairs()[validator_index as usize].sk; From 49c52001af6b9a2ed9f6f2a3517281b34dd85620 Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 21 Aug 2026 15:31:21 +0300 Subject: [PATCH 11/11] Remove unnecessary clone and add is_timely as part of the gossip-verified il --- .../gossip_verified_inclusion_list.rs | 9 +++++---- beacon_node/http_api/src/validator/mod.rs | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs b/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs index 746729e1440..23f813cd479 100644 --- a/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs +++ b/beacon_node/beacon_chain/src/inclusion_list_verification/gossip_verified_inclusion_list.rs @@ -1,6 +1,5 @@ use crate::inclusion_list_verification::InclusionListVerificationError; use crate::{BeaconChain, BeaconChainTypes}; -use std::sync::Arc; use tracing::debug; use types::{ChainSpec, SignedInclusionList}; @@ -11,17 +10,19 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { } pub struct GossipVerifiedInclusionList { - pub signed_inclusion_list: Arc, + pub signed_inclusion_list: SignedInclusionList, + pub is_timely: bool, } impl GossipVerifiedInclusionList { pub fn new( - signed_inclusion_list: Arc, + signed_inclusion_list: SignedInclusionList, _ctx: &GossipVerificationContext<'_, T>, ) -> Result { // TODO(heze): implement gossip verification for inclusion lists Ok(Self { signed_inclusion_list, + is_timely: true, }) } } @@ -36,7 +37,7 @@ impl BeaconChain { pub fn verify_inclusion_list_for_gossip( &self, - signed_inclusion_list: Arc, + signed_inclusion_list: SignedInclusionList, ) -> Result { let slot = signed_inclusion_list.message.slot; let validator_index = signed_inclusion_list.message.validator_index; diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 8329712c9c1..9c4ceacce05 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1399,12 +1399,12 @@ fn publish_inclusion_list( )); } - match chain.verify_inclusion_list_for_gossip(Arc::new(signed_inclusion_list)) { + match chain.verify_inclusion_list_for_gossip(signed_inclusion_list) { Ok(verified_inclusion_list) => { crate::utils::publish_pubsub_message( network_tx, PubsubMessage::InclusionList(Box::new( - (*verified_inclusion_list.signed_inclusion_list).clone(), + verified_inclusion_list.signed_inclusion_list, )), )?; Ok(())