From abac19c6957eeb8c7eb3d516ecbbb2c44b9e9804 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 22 Jul 2026 13:53:29 +0300 Subject: [PATCH 01/30] Add support for Heze block production --- .../src/block_production/gloas.rs | 56 ++++++++++++++----- beacon_node/beacon_chain/tests/store_tests.rs | 52 +++++++++++++++++ .../execution_layer/src/engine_api/http.rs | 10 +--- beacon_node/execution_layer/src/lib.rs | 4 -- 4 files changed, 97 insertions(+), 25 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index ed21c37df3c..dd7577c5ce6 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -29,12 +29,13 @@ use tree_hash::TreeHash; use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; use types::{ Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, - BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError, - BuilderIndex, ChainSpec, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, - ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequestsGloas, FullPayload, Graffiti, - Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, - SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, - SignedVoluntaryExit, Slot, SyncAggregate, Withdrawal, Withdrawals, + BeaconBlock, BeaconBlockBodyGloas, BeaconBlockBodyHeze, BeaconBlockGloas, BeaconBlockHeze, + BeaconState, BeaconStateError, BuilderIndex, ChainSpec, Deposit, Eth1Data, EthSpec, + ExecutionBlockHash, ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, + ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, PayloadAttestation, ProposerSlashing, + RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot, SyncAggregate, Withdrawal, + Withdrawals, }; use crate::pending_payload_envelopes::PendingEnvelopeData; @@ -600,13 +601,42 @@ impl BeaconChain { _phantom: PhantomData::>, }, }), - // TODO(heze): construct a `BeaconBlockHeze` here once Heze block production is - // wired up end-to-end (get_payload, envelope handling, etc). - BeaconState::Heze(_) => { - return Err(BlockProductionError::InvalidBlockVariant( - "Block production disabled for Heze".to_owned(), - )); - } + BeaconState::Heze(_) => BeaconBlock::Heze(BeaconBlockHeze { + slot, + proposer_index, + parent_root, + state_root: Hash256::ZERO, + body: BeaconBlockBodyHeze { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + sync_aggregate, + bls_to_execution_changes: bls_to_execution_changes + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + parent_execution_requests, + signed_execution_payload_bid, + payload_attestations: payload_attestations + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + _phantom: PhantomData::>, + }, + }), }; let signed_beacon_block = SignedBeaconBlock::from_block( diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 98b2816dc11..0f56a2a3ac5 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1929,6 +1929,58 @@ async fn proposer_lookahead_excludes_slashed_proposer_only_after_first_two_gloas ); } +/// Ensure the harness can produce and import a chain across the Heze fork boundary +#[tokio::test] +async fn heze_block_production_across_boundary() { + let gloas_fork_epoch = Epoch::new(1); + let heze_fork_epoch = Epoch::new(2); + let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + spec.gloas_fork_epoch = Some(gloas_fork_epoch); + spec.heze_fork_epoch = Some(heze_fork_epoch); + + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, Default::default(), spec.clone()); + let validators_keypairs = + types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); + let harness = TestHarness::builder(E::default()) + .spec(spec.into()) + .keypairs(validators_keypairs) + .fresh_disk_store(store) + .mock_execution_layer() + .build(); + let all_validators = harness.get_all_validators(); + + // Build a full epoch past the Heze boundary + let last_slot = (heze_fork_epoch + 1).start_slot(E::slots_per_epoch()); + let slots: Vec = (1..=last_slot.as_u64()).map(Into::into).collect(); + let state = harness.get_current_state(); + let (_, _, head_block_root, head_state) = harness + .add_attested_blocks_at_slots(state, &slots, &all_validators) + .await; + + assert_eq!(head_state.current_epoch(), heze_fork_epoch + 1); + + let head_block_root: Hash256 = head_block_root.into(); + let head_block = harness + .chain + .store + .get_blinded_block(&head_block_root) + .unwrap() + .expect("head block should be stored"); + assert!( + matches!(head_block, SignedBeaconBlock::Heze(_)), + "the head block should be a Heze block" + ); + assert!( + harness + .chain + .get_payload_envelope(&head_block_root) + .unwrap() + .is_some(), + "the Heze block's execution payload envelope should be stored" + ); +} + // Ensure blocks from abandoned forks are pruned from the Hot DB #[tokio::test] async fn prunes_abandoned_fork_between_two_finalized_checkpoints() { diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 76f172e3f72..cf0290cb4f2 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -1066,7 +1066,7 @@ impl HttpJsonRpc { let params = json!([JsonPayloadIdRequest::from(payload_id)]); match fork_name { - ForkName::Gloas => { + ForkName::Gloas | ForkName::Heze => { let response: JsonGetPayloadResponseGloas = self .rpc_request( ENGINE_GET_PAYLOAD_V6, @@ -1078,7 +1078,6 @@ impl HttpJsonRpc { .try_into() .map_err(Error::BadResponse) } - // TODO(heze): add a Heze arm once Heze payload retrieval is implemented. _ => Err(Error::UnsupportedForkVariant(format!( "called get_payload_v6 with {}", fork_name @@ -1449,18 +1448,13 @@ impl HttpJsonRpc { Err(Error::RequiredMethodUnsupported("engine_getPayloadv5")) } } - ForkName::Gloas => { + ForkName::Gloas | ForkName::Heze => { if engine_capabilities.get_payload_v6 { self.get_payload_v6(fork_name, payload_id).await } else { Err(Error::RequiredMethodUnsupported("engine_getPayloadV6")) } } - // TODO(heze): implement the Heze getPayload path once the engine API for Heze - // is specified. - ForkName::Heze => Err(Error::UnsupportedForkVariant( - "getPayload not implemented for Heze".to_string(), - )), ForkName::Base | ForkName::Altair => Err(Error::UnsupportedForkVariant(format!( "called get_payload with {}", fork_name diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index c0a684611af..cf0829001f1 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -223,8 +223,6 @@ impl From> for BlockProposalContentsGloas } } -// TODO(heze): add a `BlockProposalContentsHeze` here once Heze block production is wired up. - pub enum BlockProposalContents> { Payload { payload: Payload, @@ -943,8 +941,6 @@ impl ExecutionLayer { Ok(payload_response.into()) } - // TODO(heze): add a `get_payload_heze` here once Heze block production is wired up. - /// Maps to the `engine_getPayload` JSON-RPC call. /// /// However, it will attempt to call `self.prepare_payload` if it cannot find an existing From 4b5074f4e05dcb058358e344712809f6845ac49c Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 22 Jul 2026 16:12:15 +0300 Subject: [PATCH 02/30] Use latest fork for state_by_root_pruned_from_fork_choice --- beacon_node/http_api/tests/interactive_tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index e675ad68641..437da7d4ea6 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -61,8 +61,7 @@ async fn state_by_root_pruned_from_fork_choice() { type E = MinimalEthSpec; let validator_count = 24; - // TODO(heze): use `ForkName::latest()` once Heze block production is wired up. - let spec = ForkName::Gloas.make_genesis_spec(E::default_spec()); + let spec = ForkName::latest().make_genesis_spec(E::default_spec()); let tester = InteractiveTester::::new_with_initializer_and_mutator( Some(spec.clone()), From d47355600be44880115623382443c2f0f7e6b629 Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 3 Aug 2026 10:48:28 +0300 Subject: [PATCH 03/30] Fix formatting --- beacon_node/beacon_chain/src/block_production/gloas.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 389bfe90546..96e35446a8a 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -30,12 +30,12 @@ use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; use types::{ Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockBodyHeze, BeaconBlockGloas, BeaconBlockHeze, - BeaconState, BeaconStateError,BlobsList, BuilderIndex, ChainSpec, Deposit, Eth1Data, EthSpec, + BeaconState, BeaconStateError, BlobsList, BuilderIndex, ChainSpec, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, - ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, KzgProofs,PayloadAttestation, ProposerSlashing, - RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, - SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot, SyncAggregate, Uint256, Withdrawal, - Withdrawals, + ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, KzgProofs, PayloadAttestation, + ProposerSlashing, RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, + SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot, + SyncAggregate, Uint256, Withdrawal, Withdrawals, }; use crate::pending_payload_envelopes::PendingEnvelopeData; From db57fb78b856ff20b1d2d1d40f6c9aa122c7b88c Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 10 Aug 2026 20:11:16 +0300 Subject: [PATCH 04/30] Add clearer comments --- beacon_node/beacon_chain/tests/store_tests.rs | 2 +- beacon_node/execution_layer/src/engine_api/http.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 4ba340c4b75..f1285fa161d 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1954,7 +1954,7 @@ async fn heze_block_production_across_boundary() { .build(); let all_validators = harness.get_all_validators(); - // Build a full epoch past the Heze boundary + // Build through the first Heze epoch, ending at the first slot of the next epoch let last_slot = (heze_fork_epoch + 1).start_slot(E::slots_per_epoch()); let slots: Vec = (1..=last_slot.as_u64()).map(Into::into).collect(); let state = harness.get_current_state(); diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index f88c5327b72..568d3ff1b15 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -1078,6 +1078,8 @@ impl HttpJsonRpc { let params = json!([JsonPayloadIdRequest::from(payload_id)]); match fork_name { + // TODO(heze): deliberately reusing the Gloas response containers while the Heze + // payload is identical to the Gloas one. Switch to the Heze types if it diverges ForkName::Gloas | ForkName::Heze => { let response: JsonGetPayloadResponseGloas = self .rpc_request( From 2bc85d6a44d97fcae68d3fc80aebbb1bf648ac07 Mon Sep 17 00:00:00 2001 From: rahulbarman Date: Tue, 18 Aug 2026 08:22:04 +0530 Subject: [PATCH 05/30] Add inclusion list gossip work type and queue --- beacon_node/beacon_processor/src/lib.rs | 13 +++++++++++++ .../beacon_processor/src/scheduler/work_queue.rs | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index c98a222cec4..5c02d20e3db 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -461,6 +461,7 @@ pub enum Work { GossipExecutionPayloadBid(BlockingFn), GossipPayloadAttestation(BlockingFn), GossipProposerPreferences(BlockingFn), + GossipInclusionList(BlockingFn), LightClientBootstrapRequest(BlockingFn), LightClientOptimisticUpdateRequest(BlockingFn), LightClientFinalityUpdateRequest(BlockingFn), @@ -524,6 +525,7 @@ pub enum WorkType { GossipExecutionPayloadBid, GossipPayloadAttestation, GossipProposerPreferences, + GossipInclusionList, LightClientBootstrapRequest, LightClientOptimisticUpdateRequest, LightClientFinalityUpdateRequest, @@ -565,6 +567,7 @@ impl Work { Work::GossipExecutionPayloadBid(_) => WorkType::GossipExecutionPayloadBid, Work::GossipPayloadAttestation(_) => WorkType::GossipPayloadAttestation, Work::GossipProposerPreferences(_) => WorkType::GossipProposerPreferences, + Work::GossipInclusionList(_) => WorkType::GossipInclusionList, Work::RpcBlock { .. } => WorkType::RpcBlock, Work::RpcBlobs { .. } => WorkType::RpcBlobs, Work::RpcCustodyColumn { .. } => WorkType::RpcCustodyColumn, @@ -1023,6 +1026,9 @@ impl BeaconProcessor { work_queues.gossip_proposer_preferences_queue.pop() { Some(item) + // Check inclusion lists. + } else if let Some(item) = work_queues.gossip_inclusion_list_queue.pop() { + Some(item) // Check RPC methods next. Status messages are needed for sync so // prioritize them over syncing requests from other peers (BlocksByRange // and BlocksByRoot) @@ -1296,6 +1302,9 @@ impl BeaconProcessor { Work::GossipProposerPreferences { .. } => work_queues .gossip_proposer_preferences_queue .push(work, work_id), + Work::GossipInclusionList { .. } => { + work_queues.gossip_inclusion_list_queue.push(work, work_id) + } Work::BlobsByRootsRequest { .. } => { work_queues.blob_broots_queue.push(work, work_id) } @@ -1411,6 +1420,9 @@ impl BeaconProcessor { WorkType::GossipProposerPreferences => { work_queues.gossip_proposer_preferences_queue.len() } + WorkType::GossipInclusionList => { + work_queues.gossip_inclusion_list_queue.len() + } WorkType::LightClientBootstrapRequest => { work_queues.lc_bootstrap_queue.len() } @@ -1616,6 +1628,7 @@ impl BeaconProcessor { | Work::GossipExecutionPayloadBid(process_fn) | Work::GossipPayloadAttestation(process_fn) | Work::GossipProposerPreferences(process_fn) + | Work::GossipInclusionList(process_fn) | Work::LightClientBootstrapRequest(process_fn) | Work::LightClientOptimisticUpdateRequest(process_fn) | Work::LightClientFinalityUpdateRequest(process_fn) diff --git a/beacon_node/beacon_processor/src/scheduler/work_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_queue.rs index 76292f0202e..bf584e3ddad 100644 --- a/beacon_node/beacon_processor/src/scheduler/work_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_queue.rs @@ -147,6 +147,7 @@ pub struct BeaconProcessorQueueLengths { gossip_execution_payload_bid_queue: usize, gossip_payload_attestation_queue: usize, gossip_proposer_preferences_queue: usize, + gossip_inclusion_list_queue: usize, lc_bootstrap_queue: usize, lc_rpc_optimistic_update_queue: usize, lc_rpc_finality_update_queue: usize, @@ -233,6 +234,8 @@ impl BeaconProcessorQueueLengths { gossip_payload_attestation_queue: 1536, // TODO(EIP-7732): verify if this is preferable queue length gossip_proposer_preferences_queue: 1024, + // TODO(heze): verify if this is preferable queue length + gossip_inclusion_list_queue: 1024, lc_gossip_finality_update_queue: 1024, lc_gossip_optimistic_update_queue: 1024, lc_bootstrap_queue: 1024, @@ -288,6 +291,7 @@ pub struct WorkQueues { pub gossip_execution_payload_bid_queue: FifoQueue>, pub gossip_payload_attestation_queue: FifoQueue>, pub gossip_proposer_preferences_queue: FifoQueue>, + pub gossip_inclusion_list_queue: FifoQueue>, pub lc_gossip_finality_update_queue: FifoQueue>, pub lc_gossip_optimistic_update_queue: FifoQueue>, pub lc_bootstrap_queue: FifoQueue>, @@ -374,6 +378,7 @@ impl WorkQueues { FifoQueue::new(queue_lengths.gossip_payload_attestation_queue); let gossip_proposer_preferences_queue = FifoQueue::new(queue_lengths.gossip_proposer_preferences_queue); + let gossip_inclusion_list_queue = FifoQueue::new(queue_lengths.gossip_inclusion_list_queue); let lc_gossip_optimistic_update_queue = FifoQueue::new(queue_lengths.lc_gossip_optimistic_update_queue); @@ -433,6 +438,7 @@ impl WorkQueues { gossip_execution_payload_bid_queue, gossip_payload_attestation_queue, gossip_proposer_preferences_queue, + gossip_inclusion_list_queue, lc_gossip_optimistic_update_queue, lc_gossip_finality_update_queue, lc_bootstrap_queue, From 374a34e3d1a0318688d2ef9264b654130ad4712d Mon Sep 17 00:00:00 2001 From: rahulbarman Date: Tue, 18 Aug 2026 08:22:18 +0530 Subject: [PATCH 06/30] Wire the inclusion_list gossip topic --- .../src/service/gossip_cache.rs | 13 ++++++++++ .../lighthouse_network/src/service/utils.rs | 1 + .../lighthouse_network/src/types/pubsub.rs | 20 ++++++++++++++-- .../lighthouse_network/src/types/topics.rs | 11 +++++++++ .../gossip_methods.rs | 24 +++++++++++++++++-- .../src/network_beacon_processor/mod.rs | 17 +++++++++++++ beacon_node/network/src/router.rs | 10 ++++++++ 7 files changed, 92 insertions(+), 4 deletions(-) diff --git a/beacon_node/lighthouse_network/src/service/gossip_cache.rs b/beacon_node/lighthouse_network/src/service/gossip_cache.rs index 3120cf89c9a..87a4a575fb3 100644 --- a/beacon_node/lighthouse_network/src/service/gossip_cache.rs +++ b/beacon_node/lighthouse_network/src/service/gossip_cache.rs @@ -46,6 +46,8 @@ pub struct GossipCache { payload_attestation: Option, /// Timeout for proposer preferences. proposer_preferences: Option, + /// Timeout for inclusion lists. + inclusion_list: Option, /// Timeout for light client finality updates. light_client_finality_update: Option, /// Timeout for light client optimistic updates. @@ -83,6 +85,8 @@ pub struct GossipCacheBuilder { payload_attestation: Option, /// Timeout for proposer preferences. proposer_preferences: Option, + /// Timeout for inclusion lists. + inclusion_list: Option, /// Timeout for light client finality updates. light_client_finality_update: Option, /// Timeout for light client optimistic updates. @@ -175,6 +179,12 @@ impl GossipCacheBuilder { self } + /// Timeout for inclusion lists. + pub fn inclusion_list_timeout(mut self, timeout: Duration) -> Self { + self.inclusion_list = Some(timeout); + self + } + /// Timeout for light client finality update messages. pub fn light_client_finality_update_timeout(mut self, timeout: Duration) -> Self { self.light_client_finality_update = Some(timeout); @@ -204,6 +214,7 @@ impl GossipCacheBuilder { execution_payload_bid, payload_attestation, proposer_preferences, + inclusion_list, light_client_finality_update, light_client_optimistic_update, } = self; @@ -224,6 +235,7 @@ impl GossipCacheBuilder { execution_payload_bid: execution_payload_bid.or(default_timeout), payload_attestation: payload_attestation.or(default_timeout), proposer_preferences: proposer_preferences.or(default_timeout), + inclusion_list: inclusion_list.or(default_timeout), light_client_finality_update: light_client_finality_update.or(default_timeout), light_client_optimistic_update: light_client_optimistic_update.or(default_timeout), } @@ -256,6 +268,7 @@ impl GossipCache { GossipKind::ProposerPreferences => self.proposer_preferences, // Relayed proofs are never queued for republication. GossipKind::ExecutionProof => None, + GossipKind::InclusionList => self.inclusion_list, GossipKind::LightClientFinalityUpdate => self.light_client_finality_update, GossipKind::LightClientOptimisticUpdate => self.light_client_optimistic_update, }; diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index d4df7a7c01f..1080ef83f60 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -289,6 +289,7 @@ pub(crate) fn create_whitelist_filter( add(PayloadAttestation); add(ProposerPreferences); add(ExecutionProof); + add(InclusionList); add(LightClientFinalityUpdate); add(LightClientOptimisticUpdate); for id in 0..spec.attestation_subnet_count { diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 0f49cc5deb7..796ec9ff6c0 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -19,8 +19,8 @@ use types::{ SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBeaconBlockFulu, SignedBeaconBlockGloas, SignedBeaconBlockHeze, SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, - SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedVoluntaryExit, - SingleAttestation, SubnetId, SyncCommitteeMessage, SyncSubnetId, + SignedExecutionPayloadEnvelope, SignedInclusionList, SignedProposerPreferences, + SignedVoluntaryExit, SingleAttestation, SubnetId, SyncCommitteeMessage, SyncSubnetId, execution::SignedExecutionProof, }; @@ -56,6 +56,8 @@ pub enum PubsubMessage { ProposerPreferences(Arc), /// Gossipsub message providing notification of an EIP-8025 execution proof. ExecutionProof(Arc), + /// Gossipsub message providing notification of a signed inclusion list. + InclusionList(Box), /// Gossipsub message providing notification of a light client finality update. LightClientFinalityUpdate(Box>), /// Gossipsub message providing notification of a light client optimistic update. @@ -181,6 +183,7 @@ impl PubsubMessage { PubsubMessage::ExecutionPayloadBid(_) => GossipKind::ExecutionPayloadBid, PubsubMessage::ProposerPreferences(_) => GossipKind::ProposerPreferences, PubsubMessage::ExecutionProof(_) => GossipKind::ExecutionProof, + PubsubMessage::InclusionList(_) => GossipKind::InclusionList, PubsubMessage::LightClientFinalityUpdate(_) => GossipKind::LightClientFinalityUpdate, PubsubMessage::LightClientOptimisticUpdate(_) => { GossipKind::LightClientOptimisticUpdate @@ -447,6 +450,11 @@ impl PubsubMessage { .map_err(|e| format!("{:?}", e))?; Ok(PubsubMessage::ExecutionProof(Arc::new(execution_proof))) } + GossipKind::InclusionList => { + let inclusion_list = SignedInclusionList::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?; + Ok(PubsubMessage::InclusionList(Box::new(inclusion_list))) + } GossipKind::LightClientFinalityUpdate => { let light_client_finality_update = match fork_context .get_fork_from_context_bytes(gossip_topic.fork_digest) @@ -513,6 +521,7 @@ impl PubsubMessage { PubsubMessage::ExecutionPayloadBid(data) => data.as_ssz_bytes(), PubsubMessage::ProposerPreferences(data) => data.as_ssz_bytes(), PubsubMessage::ExecutionProof(data) => data.as_ssz_bytes(), + PubsubMessage::InclusionList(data) => data.as_ssz_bytes(), PubsubMessage::LightClientFinalityUpdate(data) => data.as_ssz_bytes(), PubsubMessage::LightClientOptimisticUpdate(data) => data.as_ssz_bytes(), } @@ -670,6 +679,13 @@ impl std::fmt::Display for PubsubMessage { data.message.beacon_block_root, data.message.proof_type, data.validator_index ) } + PubsubMessage::InclusionList(data) => { + write!( + f, + "Inclusion list: slot: {:?}, validator_index: {:?}", + data.message.slot, data.message.validator_index + ) + } PubsubMessage::LightClientFinalityUpdate(_data) => { write!(f, "Light CLient Finality Update") } diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index e8d3e97e51d..73e7d7048e0 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -33,6 +33,7 @@ pub const EXECUTION_PAYLOAD_BID: &str = "execution_payload_bid"; pub const PAYLOAD_ATTESTATION: &str = "payload_attestation_message"; pub const PROPOSER_PREFERENCES: &str = "proposer_preferences"; pub const EXECUTION_PROOF: &str = "execution_proof"; +pub const INCLUSION_LIST: &str = "inclusion_list"; pub const LIGHT_CLIENT_FINALITY_UPDATE: &str = "light_client_finality_update"; pub const LIGHT_CLIENT_OPTIMISTIC_UPDATE: &str = "light_client_optimistic_update"; @@ -99,6 +100,10 @@ pub fn core_topics_to_subscribe( } } + if fork_name.heze_enabled() { + topics.push(GossipKind::InclusionList); + } + topics } @@ -126,6 +131,7 @@ pub fn is_fork_non_core_topic(topic: &GossipTopic, _fork_name: ForkName) -> bool | GossipKind::PayloadAttestation | GossipKind::ProposerPreferences | GossipKind::ExecutionProof + | GossipKind::InclusionList | GossipKind::LightClientFinalityUpdate | GossipKind::LightClientOptimisticUpdate => false, } @@ -192,6 +198,8 @@ pub enum GossipKind { ProposerPreferences, /// Topic for EIP-8025 execution proofs. ExecutionProof, + /// Topic for signed inclusion lists. + InclusionList, /// Topic for publishing finality updates for light clients. LightClientFinalityUpdate, /// Topic for publishing optimistic updates for light clients. @@ -287,6 +295,7 @@ impl GossipTopic { PAYLOAD_ATTESTATION => GossipKind::PayloadAttestation, PROPOSER_PREFERENCES => GossipKind::ProposerPreferences, EXECUTION_PROOF => GossipKind::ExecutionProof, + INCLUSION_LIST => GossipKind::InclusionList, LIGHT_CLIENT_FINALITY_UPDATE => GossipKind::LightClientFinalityUpdate, LIGHT_CLIENT_OPTIMISTIC_UPDATE => GossipKind::LightClientOptimisticUpdate, topic => match subnet_topic_index(topic) { @@ -354,6 +363,7 @@ impl std::fmt::Display for GossipTopic { GossipKind::ExecutionPayloadBid => EXECUTION_PAYLOAD_BID.into(), GossipKind::ProposerPreferences => PROPOSER_PREFERENCES.into(), GossipKind::ExecutionProof => EXECUTION_PROOF.into(), + GossipKind::InclusionList => INCLUSION_LIST.into(), GossipKind::LightClientFinalityUpdate => LIGHT_CLIENT_FINALITY_UPDATE.into(), GossipKind::LightClientOptimisticUpdate => LIGHT_CLIENT_OPTIMISTIC_UPDATE.into(), }; @@ -428,6 +438,7 @@ mod tests { VoluntaryExit, ProposerSlashing, AttesterSlashing, + InclusionList, ] .iter() { diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 3480cf80653..c1dac48a504 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -55,8 +55,8 @@ use types::{ LightClientOptimisticUpdate, PartialDataColumn, PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, - SignedProposerPreferences, SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, - SyncCommitteeMessage, SyncSubnetId, block::BlockImportSource, data::CellBitmap, + SignedInclusionList, SignedProposerPreferences, SignedVoluntaryExit, SingleAttestation, Slot, + SubnetId, SyncCommitteeMessage, SyncSubnetId, block::BlockImportSource, data::CellBitmap, execution::SignedExecutionProof, }; @@ -4295,6 +4295,26 @@ impl NetworkBeaconProcessor { } } + #[instrument( + level = "trace", + skip_all, + fields( + peer_id = %peer_id, + slot = %inclusion_list.message.slot, + validator_index = inclusion_list.message.validator_index, + ) + )] + pub fn process_gossip_inclusion_list( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + inclusion_list: Box, + ) { + // TODO(heze): ignore every inclusion list until gossip verification lands, so that + // unverified messages are never forwarded. + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + } + #[instrument( level = "trace", skip_all, diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index b6b383b4eea..5cda29ff8cd 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -545,6 +545,23 @@ impl NetworkBeaconProcessor { }) } + /// Create a new `Work` event for some inclusion list + pub fn send_gossip_inclusion_list( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + inclusion_list: Box, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = + move || processor.process_gossip_inclusion_list(message_id, peer_id, inclusion_list); + + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::GossipInclusionList(Box::new(process_fn)), + }) + } + /// Create a new `Work` event for some block, where the result from computation (if any) is /// sent to the other side of `result_tx`. pub fn send_lookup_beacon_block( diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 665a039d2f1..2c3e4760d0d 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -580,6 +580,16 @@ impl Router { ), ) } + PubsubMessage::InclusionList(inclusion_list) => { + trace!(%peer_id, "Received a signed inclusion list"); + self.handle_beacon_processor_send_result( + self.network_beacon_processor.send_gossip_inclusion_list( + message_id, + peer_id, + inclusion_list, + ), + ) + } } } From 8b994fb490a937d4564cd03e531d658a687006c7 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 19 Aug 2026 10:58:51 +0300 Subject: [PATCH 07/30] 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 08/30] 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 09/30] 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 10/30] 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 11/30] 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 12/30] 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 13/30] 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 14/30] 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 15/30] 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 16/30] 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 17/30] 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(()) From 9bfb7cfe64535c1560144d985c95e9e99117e7e1 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 27 Aug 2026 20:14:48 +0300 Subject: [PATCH 18/30] Add PayloadAttributesV5 --- beacon_node/beacon_chain/src/beacon_chain.rs | 1 + .../src/block_production/gloas.rs | 1 + .../beacon_chain/src/execution_payload.rs | 1 + beacon_node/execution_layer/src/engine_api.rs | 55 ++++++++++++++++--- .../execution_layer/src/engine_api/http.rs | 3 + .../src/engine_api/json_structures.rs | 32 +++++++++-- .../test_utils/execution_block_generator.rs | 3 + .../src/test_utils/mock_builder.rs | 15 ++++- .../src/test_utils/mock_execution_layer.rs | 3 + .../src/test_rig.rs | 4 ++ 10 files changed, 103 insertions(+), 15 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 728a09a475a..4944887e98b 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -6689,6 +6689,7 @@ impl BeaconChain { parent_beacon_block_root, slot_number, target_gas_limit, + None, // TODO(heze): add inclusion list transactions ); execution_layer diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 655db71ea18..c7c2ca8070d 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -1157,6 +1157,7 @@ where Some(parent_beacon_block_root), slot_number, Some(target_gas_limit), + None, // TODO(heze): add inclusion list transactions ); let payload_parameters = PayloadParameters { parent_hash: parent_block_hash, diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index d8cd3e0287e..d65b309daa2 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -420,6 +420,7 @@ where parent_beacon_block_root, None, None, + None, ); let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await; diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 048e232d567..eca8eab5a46 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -26,7 +26,7 @@ pub use types::{ use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionPayloadHeze, - ExecutionRequests, KzgProofs, + ExecutionRequests, KzgProofs, ProgressiveTransactions, }; use types::{GRAFFITI_BYTES_LEN, Graffiti}; @@ -151,7 +151,7 @@ impl ExecutionBlock { } #[superstruct( - variants(V1, V2, V3, V4), + variants(V1, V2, V3, V4, V5), variant_attributes(derive(Clone, Debug, Eq, Hash, PartialEq),), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") @@ -164,14 +164,16 @@ pub struct PayloadAttributes { pub prev_randao: Hash256, #[superstruct(getter(copy))] pub suggested_fee_recipient: Address, - #[superstruct(only(V2, V3, V4))] + #[superstruct(only(V2, V3, V4, V5))] pub withdrawals: Vec, - #[superstruct(only(V3, V4), partial_getter(copy))] + #[superstruct(only(V3, V4, V5), partial_getter(copy))] pub parent_beacon_block_root: Hash256, - #[superstruct(only(V4), partial_getter(copy))] + #[superstruct(only(V4, V5), partial_getter(copy))] pub slot_number: u64, - #[superstruct(only(V4), partial_getter(copy))] + #[superstruct(only(V4, V5), partial_getter(copy))] pub target_gas_limit: u64, + #[superstruct(only(V5))] + pub inclusion_list_transactions: ProgressiveTransactions, } impl PayloadAttributes { @@ -183,18 +185,37 @@ impl PayloadAttributes { parent_beacon_block_root: Option, slot_number: Option, target_gas_limit: Option, + inclusion_list_transactions: Option, ) -> Self { match ( withdrawals, parent_beacon_block_root, slot_number, target_gas_limit, + inclusion_list_transactions, ) { ( Some(withdrawals), Some(parent_beacon_block_root), Some(slot_number), Some(target_gas_limit), + Some(inclusion_list_transactions), + ) => PayloadAttributes::V5(PayloadAttributesV5 { + timestamp, + prev_randao, + suggested_fee_recipient, + withdrawals, + parent_beacon_block_root, + slot_number, + target_gas_limit, + inclusion_list_transactions, + }), + ( + Some(withdrawals), + Some(parent_beacon_block_root), + Some(slot_number), + Some(target_gas_limit), + _, ) => PayloadAttributes::V4(PayloadAttributesV4 { timestamp, prev_randao, @@ -204,7 +225,7 @@ impl PayloadAttributes { slot_number, target_gas_limit, }), - (Some(withdrawals), Some(parent_beacon_block_root), _, _) => { + (Some(withdrawals), Some(parent_beacon_block_root), _, _, _) => { PayloadAttributes::V3(PayloadAttributesV3 { timestamp, prev_randao, @@ -213,13 +234,13 @@ impl PayloadAttributes { parent_beacon_block_root, }) } - (Some(withdrawals), None, _, _) => PayloadAttributes::V2(PayloadAttributesV2 { + (Some(withdrawals), None, _, _, _) => PayloadAttributes::V2(PayloadAttributesV2 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, }), - (None, _, _, _) => PayloadAttributes::V1(PayloadAttributesV1 { + (None, _, _, _, _) => PayloadAttributes::V1(PayloadAttributesV1 { timestamp, prev_randao, suggested_fee_recipient, @@ -280,6 +301,22 @@ impl From for SsePayloadAttributes { withdrawals, parent_beacon_block_root, }), + PayloadAttributes::V5(PayloadAttributesV5 { + timestamp, + prev_randao, + suggested_fee_recipient, + withdrawals, + parent_beacon_block_root, + slot_number: _, + target_gas_limit: _, + inclusion_list_transactions: _, + }) => Self::V3(SsePayloadAttributesV3 { + timestamp, + prev_randao, + suggested_fee_recipient, + withdrawals, + parent_beacon_block_root, + }), } } } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 23510e1b0ee..f0e6f152816 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -1504,6 +1504,9 @@ impl HttpJsonRpc { )) } } + PayloadAttributes::V5(_) => { + todo!("engine_forkchoiceUpdatedV5 not yet wired up"); + } } } else if engine_capabilities.forkchoice_updated_v4 { self.forkchoice_updated_v4(forkchoice_state, maybe_payload_attributes) diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 34c8f72489f..5bf75270096 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -905,7 +905,7 @@ impl<'a> From<&'a JsonWithdrawal> for EncodableJsonWithdrawal<'a> { } #[superstruct( - variants(V1, V2, V3, V4), + variants(V1, V2, V3, V4, V5), variant_attributes( derive(Debug, Clone, PartialEq, Serialize, Deserialize), serde(rename_all = "camelCase") @@ -921,16 +921,18 @@ pub struct JsonPayloadAttributes { pub prev_randao: Hash256, #[serde(with = "serde_utils::address_hex")] pub suggested_fee_recipient: Address, - #[superstruct(only(V2, V3, V4))] + #[superstruct(only(V2, V3, V4, V5))] pub withdrawals: Vec, - #[superstruct(only(V3, V4))] + #[superstruct(only(V3, V4, V5))] pub parent_beacon_block_root: Hash256, - #[superstruct(only(V4))] + #[superstruct(only(V4, V5))] #[serde(with = "serde_utils::u64_hex_be")] pub slot_number: u64, - #[superstruct(only(V4))] + #[superstruct(only(V4, V5))] #[serde(with = "serde_utils::u64_hex_be")] pub target_gas_limit: u64, + #[superstruct(only(V5))] + pub inclusion_list_transactions: ProgressiveTransactions, } impl From for JsonPayloadAttributes { @@ -963,6 +965,16 @@ impl From for JsonPayloadAttributes { slot_number: pa.slot_number, target_gas_limit: pa.target_gas_limit, }), + PayloadAttributes::V5(pa) => Self::V5(JsonPayloadAttributesV5 { + timestamp: pa.timestamp, + prev_randao: pa.prev_randao, + suggested_fee_recipient: pa.suggested_fee_recipient, + withdrawals: pa.withdrawals.into_iter().map(Into::into).collect(), + parent_beacon_block_root: pa.parent_beacon_block_root, + slot_number: pa.slot_number, + target_gas_limit: pa.target_gas_limit, + inclusion_list_transactions: pa.inclusion_list_transactions, + }), } } } @@ -997,6 +1009,16 @@ impl From for PayloadAttributes { slot_number: jpa.slot_number, target_gas_limit: jpa.target_gas_limit, }), + JsonPayloadAttributes::V5(jpa) => Self::V5(PayloadAttributesV5 { + timestamp: jpa.timestamp, + prev_randao: jpa.prev_randao, + suggested_fee_recipient: jpa.suggested_fee_recipient, + withdrawals: jpa.withdrawals.into_iter().map(Into::into).collect(), + parent_beacon_block_root: jpa.parent_beacon_block_root, + slot_number: jpa.slot_number, + target_gas_limit: jpa.target_gas_limit, + inclusion_list_transactions: jpa.inclusion_list_transactions, + }), } } } diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 1fcff6806f2..dbd628af8f0 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -803,6 +803,9 @@ impl ExecutionBlockGenerator { block_access_list: ProgressiveVariableList::empty(), slot_number: pa.slot_number.into(), }), + _ => unreachable!(), + }, + PayloadAttributes::V5(pa) => match self.get_fork_at_timestamp(pa.timestamp) { ForkName::Heze => ExecutionPayload::Heze(ExecutionPayloadHeze { parent_hash: head_block_hash, fee_recipient: pa.suggested_fee_recipient, diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 65cb259d25f..7de3f59bd16 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -922,6 +922,7 @@ impl MockBuilder { None, None, None, + None, ), ForkName::Deneb | ForkName::Electra | ForkName::Fulu => PayloadAttributes::new( timestamp, @@ -931,8 +932,9 @@ impl MockBuilder { Some(head_block_root), None, None, + None, ), - ForkName::Gloas | ForkName::Heze => PayloadAttributes::new( + ForkName::Gloas => PayloadAttributes::new( timestamp, *prev_randao, fee_recipient, @@ -940,6 +942,17 @@ impl MockBuilder { Some(head_block_root), Some(slot.as_u64()), None, // TODO(gloas): pass target_gas_limit + None, + ), + ForkName::Heze => PayloadAttributes::new( + timestamp, + *prev_randao, + fee_recipient, + expected_withdrawals, + Some(head_block_root), + Some(slot.as_u64()), + None, + None, // TODO(heze): pass inclusion_list_transactions ), ForkName::Base | ForkName::Altair => { return Err("invalid fork".to_string()); diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index cb1b3a210cb..e4b96b1f4ad 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -109,6 +109,7 @@ impl MockExecutionLayer { None, None, None, + None, ); // Insert a proposer to ensure the fork choice updated command works. @@ -151,6 +152,7 @@ impl MockExecutionLayer { None, None, None, + None, ); let payload_parameters = PayloadParameters { @@ -205,6 +207,7 @@ impl MockExecutionLayer { None, None, None, + None, ); let payload_parameters = PayloadParameters { diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 61f25e63a1e..e9c8ca9cae9 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -321,6 +321,7 @@ impl TestRig { None, None, None, + None, ), ) .await; @@ -368,6 +369,7 @@ impl TestRig { None, None, None, + None, ); let payload_parameters = PayloadParameters { @@ -530,6 +532,7 @@ impl TestRig { None, None, None, + None, ); let payload_parameters = PayloadParameters { @@ -592,6 +595,7 @@ impl TestRig { None, None, None, + None, ); let slot = Slot::new(42); let head_block_root = Hash256::repeat_byte(100); From 3394507e0f3b5a155e6516a6a675460b34670d64 Mon Sep 17 00:00:00 2001 From: rahulbarman Date: Fri, 28 Aug 2026 02:47:41 +0530 Subject: [PATCH 19/30] Guard inclusion list decode size and raise its queue priority --- beacon_node/beacon_processor/src/lib.rs | 6 +++--- .../lighthouse_network/src/types/pubsub.rs | 16 ++++++++++++++++ consensus/types/src/core/eth_spec.rs | 5 +++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index 5c02d20e3db..2bb31c2eb31 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -998,6 +998,9 @@ impl BeaconProcessor { work_queues.gossip_payload_attestation_queue.pop() { Some(item) + // Check inclusion lists next, they also influence fork choice. + } else if let Some(item) = work_queues.gossip_inclusion_list_queue.pop() { + Some(item) // Check sync committee messages after attestations as their rewards are lesser // and they don't influence fork choice. } else if let Some(item) = work_queues.sync_contribution_queue.pop() { @@ -1026,9 +1029,6 @@ impl BeaconProcessor { work_queues.gossip_proposer_preferences_queue.pop() { Some(item) - // Check inclusion lists. - } else if let Some(item) = work_queues.gossip_inclusion_list_queue.pop() { - Some(item) // Check RPC methods next. Status messages are needed for sync so // prioritize them over syncing requests from other peers (BlocksByRange // and BlocksByRoot) diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 796ec9ff6c0..d395e010bba 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -451,6 +451,13 @@ impl PubsubMessage { Ok(PubsubMessage::ExecutionProof(Arc::new(execution_proof))) } GossipKind::InclusionList => { + if data.len() > E::max_signed_inclusion_list_size() { + return Err(format!( + "SignedInclusionList size {} exceeds MAX_SIGNED_INCLUSION_LIST_SIZE {}", + data.len(), + E::max_signed_inclusion_list_size() + )); + } let inclusion_list = SignedInclusionList::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?; Ok(PubsubMessage::InclusionList(Box::new(inclusion_list))) @@ -844,4 +851,13 @@ mod tests { "{err}" ); } + + #[test] + fn heze_inclusion_list_size_bound() { + let max = E::max_signed_inclusion_list_size(); + let err = decode_oversized(GossipKind::InclusionList, max + 1).unwrap_err(); + assert!(err.contains("MAX_SIGNED_INCLUSION_LIST_SIZE"), "{err}"); + let err = decode_oversized(GossipKind::InclusionList, max).unwrap_err(); + assert!(!err.contains("MAX_SIGNED_INCLUSION_LIST_SIZE"), "{err}"); + } } diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index fd352df7c84..bc13a30475f 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -466,6 +466,11 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + /// Returns the `MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE` constant for this specification. fn max_signed_execution_payload_bid_size() -> usize; + /// Returns the `MAX_SIGNED_INCLUSION_LIST_SIZE` constant. + fn max_signed_inclusion_list_size() -> usize { + 8348 + } + /// Returns the `PAYLOAD_TIMELY_THRESHOLD` constant (PTC_SIZE / 2). fn payload_timely_threshold() -> usize { Self::PTCSize::to_usize() / 2 From 0c2095485a5d906c0f72b60f67dd0044e0ddd67e Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 28 Aug 2026 12:12:31 +0300 Subject: [PATCH 20/30] Add support for engine_forkchoiceUpdatedV5 --- .../tests/payload_invalidation.rs | 1 + beacon_node/execution_layer/src/engine_api.rs | 15 +++++--- .../execution_layer/src/engine_api/http.rs | 36 ++++++++++++++++++- .../src/engine_api/json_structures.rs | 1 + .../src/test_utils/handle_rpc.rs | 12 +++++-- .../execution_layer/src/test_utils/mod.rs | 1 + 6 files changed, 57 insertions(+), 9 deletions(-) diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 348922a9e07..c1b01e1b7f3 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1019,6 +1019,7 @@ async fn payload_preparation() { None, None, None, + None, ); assert_eq!(rig.previous_payload_attributes(), payload_attributes); } diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index eca8eab5a46..1f7233187ee 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -1,11 +1,12 @@ use crate::engines::ForkchoiceState; use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, - ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V2, ENGINE_GET_CLIENT_VERSION_V1, - ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, - ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, - ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, - ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5, + ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_FORKCHOICE_UPDATED_V5, ENGINE_GET_BLOBS_V2, + ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, + ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, + ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, + ENGINE_NEW_PAYLOAD_V5, }; use eth2::types::{ BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2, @@ -640,6 +641,7 @@ pub struct EngineCapabilities { pub forkchoice_updated_v2: bool, pub forkchoice_updated_v3: bool, pub forkchoice_updated_v4: bool, + pub forkchoice_updated_v5: bool, pub get_payload_bodies_by_hash_v1: bool, pub get_payload_bodies_by_range_v1: bool, pub get_payload_v1: bool, @@ -683,6 +685,9 @@ impl EngineCapabilities { if self.forkchoice_updated_v4 { response.push(ENGINE_FORKCHOICE_UPDATED_V4); } + if self.forkchoice_updated_v5 { + response.push(ENGINE_FORKCHOICE_UPDATED_V5); + } if self.get_payload_bodies_by_hash_v1 { response.push(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1); } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index f0e6f152816..2b41c9118db 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -51,6 +51,7 @@ pub const ENGINE_FORKCHOICE_UPDATED_V1: &str = "engine_forkchoiceUpdatedV1"; pub const ENGINE_FORKCHOICE_UPDATED_V2: &str = "engine_forkchoiceUpdatedV2"; pub const ENGINE_FORKCHOICE_UPDATED_V3: &str = "engine_forkchoiceUpdatedV3"; pub const ENGINE_FORKCHOICE_UPDATED_V4: &str = "engine_forkchoiceUpdatedV4"; +pub const ENGINE_FORKCHOICE_UPDATED_V5: &str = "engine_forkchoiceUpdatedV5"; pub const ENGINE_FORKCHOICE_UPDATED_TIMEOUT: Duration = Duration::from_secs(8); pub const ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1: &str = "engine_getPayloadBodiesByHashV1"; @@ -89,6 +90,7 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, ENGINE_FORKCHOICE_UPDATED_V4, + ENGINE_FORKCHOICE_UPDATED_V5, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_CLIENT_VERSION_V1, @@ -1165,6 +1167,27 @@ impl HttpJsonRpc { Ok(response.into()) } + pub async fn forkchoice_updated_v5( + &self, + forkchoice_state: ForkchoiceState, + payload_attributes: Option, + ) -> Result { + let params = json!([ + JsonForkchoiceStateV1::from(forkchoice_state), + payload_attributes.map(JsonPayloadAttributes::from) + ]); + + let response: JsonForkchoiceUpdatedV1Response = self + .rpc_request( + ENGINE_FORKCHOICE_UPDATED_V5, + params, + ENGINE_FORKCHOICE_UPDATED_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + + Ok(response.into()) + } + pub async fn get_payload_bodies_by_hash_v1( &self, block_hashes: Vec, @@ -1238,6 +1261,7 @@ impl HttpJsonRpc { forkchoice_updated_v2: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V2), forkchoice_updated_v3: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V3), forkchoice_updated_v4: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V4), + forkchoice_updated_v5: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V5), get_payload_bodies_by_hash_v1: capabilities .contains(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1), get_payload_bodies_by_range_v1: capabilities @@ -1505,9 +1529,19 @@ impl HttpJsonRpc { } } PayloadAttributes::V5(_) => { - todo!("engine_forkchoiceUpdatedV5 not yet wired up"); + if engine_capabilities.forkchoice_updated_v5 { + self.forkchoice_updated_v5(forkchoice_state, maybe_payload_attributes) + .await + } else { + Err(Error::RequiredMethodUnsupported( + "engine_forkchoiceUpdatedV5", + )) + } } } + } else if engine_capabilities.forkchoice_updated_v5 { + self.forkchoice_updated_v5(forkchoice_state, maybe_payload_attributes) + .await } else if engine_capabilities.forkchoice_updated_v4 { self.forkchoice_updated_v4(forkchoice_state, maybe_payload_attributes) .await diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 5bf75270096..95b836462ed 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -932,6 +932,7 @@ pub struct JsonPayloadAttributes { #[serde(with = "serde_utils::u64_hex_be")] pub target_gas_limit: u64, #[superstruct(only(V5))] + #[serde(with = "ssz_types::serde_utils::prog_list_of_hex_prog_var_list")] pub inclusion_list_transactions: ProgressiveTransactions, } 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 2f4ad9fdaa3..f06321a8e30 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -531,7 +531,8 @@ pub async fn handle_rpc( ENGINE_FORKCHOICE_UPDATED_V1 | ENGINE_FORKCHOICE_UPDATED_V2 | ENGINE_FORKCHOICE_UPDATED_V3 - | ENGINE_FORKCHOICE_UPDATED_V4 => { + | ENGINE_FORKCHOICE_UPDATED_V4 + | ENGINE_FORKCHOICE_UPDATED_V5 => { let forkchoice_state: JsonForkchoiceStateV1 = get_param(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; let payload_attributes = match method { @@ -583,6 +584,11 @@ pub async fn handle_rpc( .map(|opt| opt.map(JsonPayloadAttributes::V4)) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))? } + ENGINE_FORKCHOICE_UPDATED_V5 => { + get_param::>(params, 1) + .map(|opt| opt.map(JsonPayloadAttributes::V5)) + .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))? + } _ => unreachable!(), }; @@ -659,9 +665,9 @@ pub async fn handle_rpc( } } ForkName::Heze => { - if method != ENGINE_FORKCHOICE_UPDATED_V4 { + if method != ENGINE_FORKCHOICE_UPDATED_V5 { return Err(( - format!("{} called after Heze fork! Use V4.", method), + format!("{} called after Heze fork! Use V5.", method), FORK_REQUEST_MISMATCH_ERROR_CODE, )); } diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 88f46cf8e77..1b84b08b0a6 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -48,6 +48,7 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { forkchoice_updated_v2: true, forkchoice_updated_v3: true, forkchoice_updated_v4: true, + forkchoice_updated_v5: true, get_payload_bodies_by_hash_v1: true, get_payload_bodies_by_range_v1: true, get_payload_v1: true, From c78fc2d796052f5d022fd49b57f9cc5259bae8b4 Mon Sep 17 00:00:00 2001 From: conache Date: Sat, 29 Aug 2026 14:55:08 +0300 Subject: [PATCH 21/30] Test the fcU call at the fork boundary --- beacon_node/beacon_chain/src/beacon_chain.rs | 9 +- .../src/block_production/gloas.rs | 16 ++- .../beacon_chain/tests/prepare_payload.rs | 132 ++++++++++++++++++ .../execution_layer/src/engine_api/http.rs | 3 +- .../test_utils/execution_block_generator.rs | 29 ++-- .../src/test_utils/mock_builder.rs | 4 +- 6 files changed, 169 insertions(+), 24 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 4944887e98b..b8d1d6c4446 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -6678,6 +6678,13 @@ impl BeaconChain { None }; + let inclusion_list_transactions = if prepare_slot_fork.heze_enabled() { + // TODO(heze): populate from the inclusion list store + Some(ProgressiveTransactions::empty()) + } else { + None + }; + let payload_attributes = PayloadAttributes::new( self.slot_clock .start_of(prepare_slot) @@ -6689,7 +6696,7 @@ impl BeaconChain { parent_beacon_block_root, slot_number, target_gas_limit, - None, // TODO(heze): add inclusion list transactions + inclusion_list_transactions, ); execution_layer diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index c7c2ca8070d..72d90cfe988 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -33,10 +33,10 @@ use types::{ BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError, BlobsList, BuilderIndex, ChainSpec, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequestsGloas, FullPayload, Graffiti, - Hash256, IndexedAttestation, KzgProofs, PayloadAttestation, ProposerSlashing, RelativeEpoch, - SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, - SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot, SyncAggregate, Uint256, Withdrawal, - Withdrawals, + Hash256, IndexedAttestation, KzgProofs, PayloadAttestation, ProgressiveTransactions, + ProposerSlashing, RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, + SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot, + SyncAggregate, Uint256, Withdrawal, Withdrawals, }; use crate::payload_bid_verification::payload_bid_cache::BidParent; @@ -1148,6 +1148,12 @@ where .get_proposer_gas_limit(proposer_index) .await .unwrap_or(DEFAULT_GAS_LIMIT); + let inclusion_list_transactions = if fork.heze_enabled() { + // TODO(heze): populate from the inclusion list store + Some(ProgressiveTransactions::empty()) + } else { + None + }; let payload_attributes = PayloadAttributes::new( timestamp, @@ -1157,7 +1163,7 @@ where Some(parent_beacon_block_root), slot_number, Some(target_gas_limit), - None, // TODO(heze): add inclusion list transactions + inclusion_list_transactions, ); let payload_parameters = PayloadParameters { parent_hash: parent_block_hash, diff --git a/beacon_node/beacon_chain/tests/prepare_payload.rs b/beacon_node/beacon_chain/tests/prepare_payload.rs index a33f4aaef40..262784bc29a 100644 --- a/beacon_node/beacon_chain/tests/prepare_payload.rs +++ b/beacon_node/beacon_chain/tests/prepare_payload.rs @@ -7,6 +7,9 @@ use beacon_chain::test_utils::{ use beacon_chain::{ChainConfig, custody_context::NodeCustodyType}; use bls::Keypair; use eth2::types::ProposerPreparationData; +use execution_layer::PayloadAttributes; +use execution_layer::http::{ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_FORKCHOICE_UPDATED_V5}; +use execution_layer::json_structures::{JsonPayloadAttributesV4, JsonPayloadAttributesV5}; use fork_choice::PayloadStatus; use logging::create_test_tracing_subscriber; use ssz_types::ProgressiveVariableList; @@ -585,6 +588,135 @@ async fn prepare_payload_on_fork_boundary( ); } +#[tokio::test] +async fn prepare_payload_on_heze_boundary() { + let heze_fork_epoch = Epoch::new(1); + prepare_payload_around_heze_boundary( + heze_fork_epoch.start_slot(E::slots_per_epoch()), + heze_fork_epoch, + ) + .await; +} + +#[tokio::test] +async fn prepare_payload_before_heze_boundary() { + let heze_fork_epoch = Epoch::new(1); + prepare_payload_around_heze_boundary( + heze_fork_epoch.start_slot(E::slots_per_epoch()) - 1, + heze_fork_epoch, + ) + .await; +} + +/// Prepare a payload for `prepare_slot` on a chain with a Heze fork at `heze_fork_epoch` and +/// verify that the fcU sent to the execution layer uses the engine API version required for the +/// fork of `prepare_slot`: `engine_forkchoiceUpdatedV5` with `PayloadAttributesV5` from Heze +/// onwards, `V4` before +async fn prepare_payload_around_heze_boundary(prepare_slot: Slot, heze_fork_epoch: Epoch) { + let mut spec = test_spec::(); + if !spec.fork_name_at_slot::(Slot::new(0)).gloas_enabled() { + return; + } + spec.gloas_fork_epoch = Some(Epoch::new(0)); + spec.heze_fork_epoch = Some(heze_fork_epoch); + let spec = Arc::new(spec); + + let prepare_slot_is_heze = spec.fork_name_at_slot::(prepare_slot).heze_enabled(); + + // Only produce blocks up to the parent slot, so no Heze block production is required + let num_blocks_produced = (prepare_slot - 1).as_u64(); + let db_path = tempdir().unwrap(); + let store = get_store(&db_path, spec.clone()); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + harness + .extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let cached_head = harness.chain.canonical_head.cached_head(); + let parent_payload_status = cached_head.head_payload_status(); + + let mut advanced_state = cached_head.snapshot.beacon_state.clone(); + complete_state_advance(&mut advanced_state, None, prepare_slot, &spec).unwrap(); + + // Call `prepare_beacon_proposer` for the next slot. + // This sends a fcU with the payload attributes to the execution layer + // which validates the method version against the fork at the attributes' timestamp + let current_slot = prepare_slot - 1; + let proposer_index = advanced_state + .get_beacon_proposer_index(prepare_slot, &spec) + .unwrap(); + + let el = harness.chain.execution_layer.as_ref().unwrap(); + el.update_proposer_preparation( + prepare_slot.epoch(E::slots_per_epoch()), + [( + &ProposerPreparationData { + validator_index: proposer_index as u64, + fee_recipient: Address::repeat_byte(42), + }, + &None, + )], + ) + .await; + + // Advance the slot clock to just before the prepare slot so the lookahead check passes + harness.advance_to_slot_lookahead(prepare_slot, harness.chain.config.prepare_payload_lookahead); + + harness + .chain + .prepare_beacon_proposer(current_slot) + .await + .unwrap(); + + // Inspect the fcU request received by the mock execution layer + let request = harness + .mock_execution_layer + .as_ref() + .unwrap() + .server + .take_previous_request() + .expect("no previous request"); + let method = request.get("method").expect("no method"); + let params = request.get("params").expect("no params"); + let payload_attributes_json = params.get(1).expect("no payload attributes param"); + + if prepare_slot_is_heze { + assert_eq!(method, ENGINE_FORKCHOICE_UPDATED_V5); + let attributes: JsonPayloadAttributesV5 = + serde_json::from_value(payload_attributes_json.clone()).unwrap(); + // We are currently sending the V5 shape with an empty inclusion list + assert!(attributes.inclusion_list_transactions.is_empty()); + // `custodyColumns` is the (nullable) third parameter of engine api call + assert!(params.get(2).is_some_and(|c| c.is_null())); + } else { + assert_eq!(method, ENGINE_FORKCHOICE_UPDATED_V4); + let _attributes: JsonPayloadAttributesV4 = + serde_json::from_value(payload_attributes_json.clone()).unwrap(); + assert!( + payload_attributes_json + .get("inclusionListTransactions") + .is_none() + ); + } + + // The cached payload attributes must use the matching variant + let head_root = harness.head_block_root(); + let attributes = el + .payload_attributes(prepare_slot, head_root, parent_payload_status) + .await + .unwrap(); + if prepare_slot_is_heze { + assert!(matches!(attributes, PayloadAttributes::V5(_))); + } else { + assert!(matches!(attributes, PayloadAttributes::V4(_))); + } +} + #[tokio::test] async fn gloas_block_production_caches_blobs_for_column_publishing() { use beacon_chain::ProduceBlockVerification; diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 2b41c9118db..ea6109d55ce 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -1174,7 +1174,8 @@ impl HttpJsonRpc { ) -> Result { let params = json!([ JsonForkchoiceStateV1::from(forkchoice_state), - payload_attributes.map(JsonPayloadAttributes::from) + payload_attributes.map(JsonPayloadAttributes::from), + null ]); let response: JsonForkchoiceUpdatedV1Response = self diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index dbd628af8f0..f92334f7742 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -845,22 +845,21 @@ impl ExecutionBlockGenerator { let max_blobs = max(1, self.min_blobs_count); let num_blobs = rng.random_range(self.min_blobs_count..=max_blobs); let (bundle, transactions) = generate_blobs(num_blobs, fork_name)?; - match &mut execution_payload { - ExecutionPayload::Gloas(payload) => { - for tx in Vec::from(transactions) { - payload - .transactions - .push(ProgressiveVariableList::::new(tx.into())); - } + // Gloas and later carry a progressive transactions list, earlier forks a bounded one. + if fork_name.gloas_enabled() { + let payload_transactions = execution_payload + .transactions_progressive_mut() + .map_err(|e| format!("invalid payload variant: {e:?}"))?; + for tx in Vec::from(transactions) { + payload_transactions.push(ProgressiveVariableList::::new(tx.into())); } - _ => { - for tx in Vec::from(transactions) { - execution_payload - .transactions_bounded_mut() - .map_err(|e| format!("invalid payload variant: {e:?}"))? - .push(tx) - .map_err(|_| "transactions are full".to_string())?; - } + } else { + for tx in Vec::from(transactions) { + execution_payload + .transactions_bounded_mut() + .map_err(|e| format!("invalid payload variant: {e:?}"))? + .push(tx) + .map_err(|_| "transactions are full".to_string())?; } } bundle diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 7de3f59bd16..b586b9dd89d 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -28,7 +28,6 @@ use tempfile::NamedTempFile; use tokio_stream::StreamExt; use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; -use types::ExecutionBlockHash; use types::builder::{ BuilderBid, BuilderBidBellatrix, BuilderBidCapella, BuilderBidDeneb, BuilderBidElectra, BuilderBidFulu, SignedBuilderBid, @@ -39,6 +38,7 @@ use types::{ ForkVersionDecode, Hash256, SignedBlindedBeaconBlock, SignedRoot, SignedValidatorRegistrationData, Slot, Uint256, }; +use types::{ExecutionBlockHash, ProgressiveTransactions}; use warp::{ Filter, Rejection, http::StatusCode, @@ -952,7 +952,7 @@ impl MockBuilder { Some(head_block_root), Some(slot.as_u64()), None, - None, // TODO(heze): pass inclusion_list_transactions + Some(ProgressiveTransactions::empty()), // TODO(heze): pass inclusion_list_transactions ), ForkName::Base | ForkName::Altair => { return Err("invalid fork".to_string()); From d0a01c3a4ffc0c5d79a4b776601b46f766ddb6d1 Mon Sep 17 00:00:00 2001 From: conache Date: Sat, 29 Aug 2026 15:49:19 +0300 Subject: [PATCH 22/30] Fix clippy error --- beacon_node/execution_layer/src/engine_api.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 1f7233187ee..6608db835df 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -178,6 +178,7 @@ pub struct PayloadAttributes { } impl PayloadAttributes { + #[allow(clippy::too_many_arguments)] pub fn new( timestamp: u64, prev_randao: Hash256, From 3ee997c3246e73f51404284bca72fcf283544adc Mon Sep 17 00:00:00 2001 From: conache Date: Sat, 29 Aug 2026 16:48:33 +0300 Subject: [PATCH 23/30] Add engine api test --- .../execution_layer/src/engine_api/http.rs | 73 ++++++++++++++++++- .../src/test_utils/mock_builder.rs | 9 +-- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index ea6109d55ce..15aeb8a818a 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -1567,7 +1567,7 @@ mod test { use super::*; use crate::test_utils::{DEFAULT_JWT_SECRET, MockServer}; use fixed_bytes::FixedBytesExtended; - use ssz_types::VariableList; + use ssz_types::{ProgressiveVariableList, VariableList}; use std::future::Future; use std::str::FromStr; use std::sync::Arc; @@ -1880,6 +1880,77 @@ mod test { .await; } + #[tokio::test] + async fn forkchoice_updated_v5_with_payload_attributes_request() { + let payload_attributes = || { + Some(PayloadAttributes::V5(PayloadAttributesV5 { + timestamp: 5, + prev_randao: Hash256::zero(), + suggested_fee_recipient: Address::repeat_byte(0), + withdrawals: vec![], + parent_beacon_block_root: Hash256::zero(), + slot_number: 7, + target_gas_limit: 30_000_000, + inclusion_list_transactions: ProgressiveVariableList::new(vec![ + ProgressiveVariableList::new(vec![0x02, 0xf8, 0x6f]), + ]), + })) + }; + + Tester::new(true) + .assert_request_equals( + |client| async move { + let _ = client + .forkchoice_updated_v5( + ForkchoiceState { + head_block_hash: ExecutionBlockHash::repeat_byte(1), + safe_block_hash: ExecutionBlockHash::repeat_byte(1), + finalized_block_hash: ExecutionBlockHash::zero(), + }, + payload_attributes(), + ) + .await; + }, + json!({ + "id": STATIC_ID, + "jsonrpc": JSONRPC_VERSION, + "method": ENGINE_FORKCHOICE_UPDATED_V5, + "params": [{ + "headBlockHash": HASH_01, + "safeBlockHash": HASH_01, + "finalizedBlockHash": HASH_00, + }, + { + "timestamp": "0x5", + "prevRandao": HASH_00, + "suggestedFeeRecipient": ADDRESS_00, + "withdrawals": [], + "parentBeaconBlockRoot": HASH_00, + "slotNumber": "0x7", + "targetGasLimit": "0x1c9c380", + "inclusionListTransactions": ["0x02f86f"], + }, + null] + }), + ) + .await; + + Tester::new(false) + .assert_auth_failure(|client| async move { + client + .forkchoice_updated_v5( + ForkchoiceState { + head_block_hash: ExecutionBlockHash::repeat_byte(1), + safe_block_hash: ExecutionBlockHash::repeat_byte(1), + finalized_block_hash: ExecutionBlockHash::zero(), + }, + payload_attributes(), + ) + .await + }) + .await; + } + #[tokio::test] async fn get_payload_v1_request() { Tester::new(true) diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index b586b9dd89d..17f5a3be1c4 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -33,12 +33,11 @@ use types::builder::{ BuilderBidFulu, SignedBuilderBid, }; use types::{ - Address, BeaconState, ChainSpec, Epoch, EthSpec, ExecPayload, ExecutionPayload, - ExecutionPayloadHeaderRefMut, ExecutionRequests, ExecutionRequestsElectra, ForkName, - ForkVersionDecode, Hash256, SignedBlindedBeaconBlock, SignedRoot, - SignedValidatorRegistrationData, Slot, Uint256, + Address, BeaconState, ChainSpec, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, + ExecutionPayload, ExecutionPayloadHeaderRefMut, ExecutionRequests, ExecutionRequestsElectra, + ForkName, ForkVersionDecode, Hash256, ProgressiveTransactions, SignedBlindedBeaconBlock, + SignedRoot, SignedValidatorRegistrationData, Slot, Uint256, }; -use types::{ExecutionBlockHash, ProgressiveTransactions}; use warp::{ Filter, Rejection, http::StatusCode, From b9387af5155eafdef0d2a3593045167998be610d Mon Sep 17 00:00:00 2001 From: conache Date: Sat, 29 Aug 2026 17:30:28 +0300 Subject: [PATCH 24/30] Remove duplicate arm --- .../src/test_utils/mock_builder.rs | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 17f5a3be1c4..f0e6847bc63 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -28,15 +28,16 @@ use tempfile::NamedTempFile; use tokio_stream::StreamExt; use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; +use types::ExecutionBlockHash; use types::builder::{ BuilderBid, BuilderBidBellatrix, BuilderBidCapella, BuilderBidDeneb, BuilderBidElectra, BuilderBidFulu, SignedBuilderBid, }; use types::{ - Address, BeaconState, ChainSpec, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, - ExecutionPayload, ExecutionPayloadHeaderRefMut, ExecutionRequests, ExecutionRequestsElectra, - ForkName, ForkVersionDecode, Hash256, ProgressiveTransactions, SignedBlindedBeaconBlock, - SignedRoot, SignedValidatorRegistrationData, Slot, Uint256, + Address, BeaconState, ChainSpec, Epoch, EthSpec, ExecPayload, ExecutionPayload, + ExecutionPayloadHeaderRefMut, ExecutionRequests, ExecutionRequestsElectra, ForkName, + ForkVersionDecode, Hash256, SignedBlindedBeaconBlock, SignedRoot, + SignedValidatorRegistrationData, Slot, Uint256, }; use warp::{ Filter, Rejection, @@ -933,7 +934,7 @@ impl MockBuilder { None, None, ), - ForkName::Gloas => PayloadAttributes::new( + ForkName::Gloas | ForkName::Heze => PayloadAttributes::new( timestamp, *prev_randao, fee_recipient, @@ -943,16 +944,6 @@ impl MockBuilder { None, // TODO(gloas): pass target_gas_limit None, ), - ForkName::Heze => PayloadAttributes::new( - timestamp, - *prev_randao, - fee_recipient, - expected_withdrawals, - Some(head_block_root), - Some(slot.as_u64()), - None, - Some(ProgressiveTransactions::empty()), // TODO(heze): pass inclusion_list_transactions - ), ForkName::Base | ForkName::Altair => { return Err("invalid fork".to_string()); } From dda724671408343a120f2aaa75aa55c1c7a2085d Mon Sep 17 00:00:00 2001 From: rahulbarman Date: Wed, 2 Sep 2026 03:38:55 +0530 Subject: [PATCH 25/30] Update MAX_SIGNED_INCLUSION_LIST_SIZE per consensus-specs#5576 --- consensus/types/src/core/eth_spec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index bc13a30475f..888d9f50a16 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -468,7 +468,7 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + /// Returns the `MAX_SIGNED_INCLUSION_LIST_SIZE` constant. fn max_signed_inclusion_list_size() -> usize { - 8348 + 41112 } /// Returns the `PAYLOAD_TIMELY_THRESHOLD` constant (PTC_SIZE / 2). From a945563c810b93750e9929ea1d27b932b68ab177 Mon Sep 17 00:00:00 2001 From: rahulbarman Date: Wed, 2 Sep 2026 04:34:33 +0530 Subject: [PATCH 26/30] Wire the inclusion list store into BeaconChain --- beacon_node/beacon_chain/src/beacon_chain.rs | 85 +++++++ beacon_node/beacon_chain/src/builder.rs | 2 + beacon_node/beacon_chain/src/errors.rs | 3 + .../beacon_chain/src/inclusion_list_store.rs | 7 +- .../tests/inclusion_list_store.rs | 239 ++++++++++++++++++ beacon_node/beacon_chain/tests/main.rs | 1 + 6 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 beacon_node/beacon_chain/tests/inclusion_list_store.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 53b3f624a9a..5066313e77c 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -37,6 +37,7 @@ use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_e use crate::execution_proof_verification::ObservedExecutionProofs; use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; +use crate::inclusion_list_store::{DependentRoot, InclusionListCommittee, InclusionListStore}; use crate::light_client_finality_update_verification::{ Error as LightClientFinalityUpdateError, VerifiedLightClientFinalityUpdate, }; @@ -122,6 +123,7 @@ use serde_utils::quoted_u64::Quoted; use slasher::Slasher; use slot_clock::SlotClock; use ssz::Encode; +use ssz_types::{BitVector, FixedVector, ProgressiveVariableList}; use state_processing::{ BlockSignatureStrategy, ConsensusContext, GloasVerificationContext, SigVerifiedOp, VerifyBlockRoot, VerifyOperation, @@ -448,6 +450,8 @@ pub struct BeaconChain { /// Cache of pending execution payload envelopes for local block building. /// Envelopes are stored here during block production and eventually published. pub pending_payload_envelopes: RwLock>, + /// Inclusion lists received over gossip for recent slots. + pub inclusion_list_store: RwLock>, /// Maintains a record of which validators have submitted voluntary exits. pub observed_voluntary_exits: Mutex>, /// Maintains a record of which validators we've seen proposer slashings for. @@ -7178,6 +7182,7 @@ impl BeaconChain { self.gossip_verified_payload_bid_cache.prune(slot); self.gossip_verified_proposer_preferences_cache.prune(slot); self.pending_payload_envelopes.write().prune(slot); + self.inclusion_list_store.write().prune(slot); // Don't run heavy-weight tasks during sync. if self.best_slot() + MAX_PER_SLOT_FORK_CHOICE_DISTANCE < slot { @@ -7297,6 +7302,86 @@ impl BeaconChain { ) } + /// The ordered inclusion list committee for `slot`, and the dependent root that keys it. + /// + /// `block_root` must not be from a later epoch than `slot`, since `with_committee_cache` cannot + /// resolve a shuffling earlier than its own block's epoch. + pub fn inclusion_list_committee( + &self, + block_root: Hash256, + slot: Slot, + ) -> Result<(InclusionListCommittee, DependentRoot), Error> { + let shuffling_epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let committee_size = T::EthSpec::inclusion_list_committee_size(); + + self.with_committee_cache( + block_root, + shuffling_epoch, + |cached_shuffling, dependent_root| { + let committee = cached_shuffling + .committee_cache + .get_inclusion_list_committee_at_slot(slot, committee_size)? + .into_iter() + .map(|index| index as u64) + .collect::>(); + + Ok((FixedVector::new(committee)?, dependent_root)) + }, + ) + } + + /// The deduplicated transactions from the inclusion lists stored for `slot`. + pub fn get_inclusion_list_transactions( + &self, + block_root: Hash256, + slot: Slot, + only_timely: bool, + ) -> Result>, Error> { + let (_, dependent_root) = self.inclusion_list_committee(block_root, slot)?; + + Ok(self + .inclusion_list_store + .read() + .get_inclusion_list_transactions(slot, dependent_root, only_timely)) + } + + /// The inclusion list committee bits for `slot` as observed by this node. + pub fn get_inclusion_list_bits( + &self, + block_root: Hash256, + slot: Slot, + only_timely: bool, + ) -> Result::InclusionListCommitteeSize>, Error> { + let (il_committee, dependent_root) = self.inclusion_list_committee(block_root, slot)?; + + self.inclusion_list_store + .read() + .get_inclusion_list_bits(slot, dependent_root, &il_committee, only_timely) + .map_err(Into::into) + } + + /// Whether `bits` covers every inclusion list this node observed for `slot`. + pub fn is_inclusion_list_bits_inclusive( + &self, + block_root: Hash256, + slot: Slot, + bits: &BitVector<::InclusionListCommitteeSize>, + only_timely: bool, + ) -> Result { + let (il_committee, dependent_root) = self.inclusion_list_committee(block_root, slot)?; + + self.inclusion_list_store + .read() + .is_inclusion_list_bits_inclusive( + slot, + dependent_root, + &il_committee, + bits, + only_timely, + ) + .map_err(Into::into) + } + /// 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/builder.rs b/beacon_node/beacon_chain/src/builder.rs index d286982992a..a51b074147a 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -8,6 +8,7 @@ use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; use crate::fork_choice_signal::ForkChoiceSignalTx; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin}; +use crate::inclusion_list_store::InclusionListStore; use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sidecars_gloas}; use crate::light_client_server_cache::LightClientServerCache; use crate::migrate::{BackgroundMigrator, MigratorConfig}; @@ -1031,6 +1032,7 @@ where observed_execution_proofs: <_>::default(), observed_execution_payloads: <_>::default(), pending_payload_envelopes: <_>::default(), + inclusion_list_store: RwLock::new(InclusionListStore::new(&self.spec)), observed_voluntary_exits: <_>::default(), observed_proposer_slashings: <_>::default(), observed_attester_slashings: <_>::default(), diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 1e82a5cba69..c8f72804668 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -2,6 +2,7 @@ use crate::beacon_block_streamer::Error as BlockStreamerError; use crate::beacon_chain::ForkChoiceError; use crate::beacon_fork_choice_store::Error as ForkChoiceStoreError; use crate::data_availability_checker::AvailabilityCheckError; +use crate::inclusion_list_store::Error as InclusionListStoreError; use crate::migrate::PruningError; use crate::naive_aggregation_pool::Error as NaiveAggregationError; use crate::observed_aggregates::Error as ObservedAttestationsError; @@ -104,6 +105,7 @@ pub enum BeaconChainError { ObservedAttestersError(ObservedAttestersError), ObservedBlockProducersError(ObservedBlockProducersError), ObservedDataSidecarsError(ObservedDataSidecarsError), + InclusionListStoreError(InclusionListStoreError), EarlyAttesterCacheError, PruningError(PruningError), ArithError(ArithError), @@ -278,6 +280,7 @@ easy_from_to!(ObservedAttestationsError, BeaconChainError); easy_from_to!(ObservedAttestersError, BeaconChainError); easy_from_to!(ObservedBlockProducersError, BeaconChainError); easy_from_to!(ObservedDataSidecarsError, BeaconChainError); +easy_from_to!(InclusionListStoreError, BeaconChainError); easy_from_to!(BlockSignatureVerifierError, BeaconChainError); easy_from_to!(PruningError, BeaconChainError); easy_from_to!(ArithError, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/inclusion_list_store.rs b/beacon_node/beacon_chain/src/inclusion_list_store.rs index f0530e66768..8f228311c0e 100644 --- a/beacon_node/beacon_chain/src/inclusion_list_store.rs +++ b/beacon_node/beacon_chain/src/inclusion_list_store.rs @@ -15,6 +15,9 @@ use types::{ChainSpec, EthSpec, Hash256, SignedInclusionList, Slot}; /// The shuffling `dependent_root` an inclusion list was produced against. pub type DependentRoot = Hash256; +/// The inclusion list committee, ordered by committee position. +pub type InclusionListCommittee = FixedVector::InclusionListCommitteeSize>; + /// The result of inserting a `SignedInclusionList`. Drives the gossip accept/ignore verdict. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum InsertOutcome { @@ -209,7 +212,7 @@ impl InclusionListStore { &self, slot: Slot, dependent_root: DependentRoot, - il_committee: &FixedVector, + il_committee: &InclusionListCommittee, only_timely: bool, ) -> Result, Error> { let submitted = self.submitted_validators(slot, dependent_root, only_timely); @@ -229,7 +232,7 @@ impl InclusionListStore { &self, slot: Slot, dependent_root: DependentRoot, - il_committee: &FixedVector, + il_committee: &InclusionListCommittee, bits: &BitVector, only_timely: bool, ) -> Result { diff --git a/beacon_node/beacon_chain/tests/inclusion_list_store.rs b/beacon_node/beacon_chain/tests/inclusion_list_store.rs new file mode 100644 index 00000000000..a8a33d0ed10 --- /dev/null +++ b/beacon_node/beacon_chain/tests/inclusion_list_store.rs @@ -0,0 +1,239 @@ +//! Tests for the inclusion list store reads on `BeaconChain`. +//! +//! These do not need a Heze state: the committee derives from the ordinary attester shuffling and +//! the store holds no fork-specific data. + +use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; +use beacon_chain::{BeaconChainError, WhenSlotSkipped}; +use bls::Signature; +use ssz_types::ProgressiveVariableList; +use types::{ + EthSpec, Hash256, InclusionList, MinimalEthSpec, RelativeEpoch, SignedInclusionList, Slot, +}; + +type E = MinimalEthSpec; + +/// 8 validators per slot on minimal, fewer than the committee size, so positions repeat. +const VALIDATOR_COUNT: usize = 64; + +fn get_harness() -> BeaconChainHarness> { + BeaconChainHarness::builder(E::default()) + .default_spec() + .deterministic_keypairs(VALIDATOR_COUNT) + .fresh_ephemeral_store() + .mock_execution_layer() + .build() +} + +fn transaction(byte: u8) -> ProgressiveVariableList { + ProgressiveVariableList::new(vec![byte]) +} + +fn signed_inclusion_list( + slot: Slot, + validator_index: u64, + dependent_root: Hash256, + tx_byte: u8, +) -> SignedInclusionList { + SignedInclusionList { + message: InclusionList { + slot, + validator_index, + dependent_root, + transactions: ProgressiveVariableList::new(vec![transaction(tx_byte)]), + }, + signature: Signature::empty(), + } +} + +#[tokio::test] +async fn committee_matches_the_state_level_helper() { + let harness = get_harness(); + let slot = Slot::new(1); + + let block_root = harness.head_block_root(); + let (committee, dependent_root) = harness + .chain + .inclusion_list_committee(block_root, slot) + .unwrap(); + + assert_eq!(committee.len(), E::inclusion_list_committee_size()); + + let mut state = harness.get_current_state(); + state + .build_committee_cache(RelativeEpoch::Current, &harness.chain.spec) + .unwrap(); + let expected = state.get_inclusion_list_committee(slot).unwrap(); + + assert_eq!(committee, expected); + + // The store is keyed on the attester shuffling decision root, which is what a producer puts on + // the inclusion list as its `dependent_root`. + assert_eq!( + dependent_root, + state + .attester_shuffling_decision_root(block_root, RelativeEpoch::Current) + .unwrap() + ); +} + +/// Anchoring on a block root from the slot's own epoch resolves, where the head root does not. +#[tokio::test] +async fn committee_resolves_for_a_slot_in_the_previous_epoch() { + let harness = get_harness(); + harness + .extend_slots(E::slots_per_epoch() as usize + 1) + .await; + + let slot = Slot::new(E::slots_per_epoch() - 1); + assert!(harness.chain.canonical_head.cached_head().head_slot() > slot); + + let block_root = harness + .chain + .block_root_at_slot(slot, WhenSlotSkipped::Prev) + .unwrap() + .unwrap(); + + let (committee, _) = harness + .chain + .inclusion_list_committee(block_root, slot) + .unwrap(); + + let mut state = harness.get_current_state(); + state + .build_committee_cache(RelativeEpoch::Previous, &harness.chain.spec) + .unwrap(); + let expected = state.get_inclusion_list_committee(slot).unwrap(); + assert_eq!(committee, expected); + + assert!(matches!( + harness + .chain + .inclusion_list_committee(harness.head_block_root(), slot), + Err(BeaconChainError::InvalidShufflingId { .. }) + )); +} + +#[tokio::test] +async fn bits_and_transactions_read_back_through_the_chain() { + let harness = get_harness(); + let slot = Slot::new(1); + let block_root = harness.head_block_root(); + + let (committee, dependent_root) = harness + .chain + .inclusion_list_committee(block_root, slot) + .unwrap(); + + let timely_submitter = committee[0]; + let late_submitter = *committee + .iter() + .find(|index| **index != timely_submitter) + .unwrap(); + + let mut store = harness.chain.inclusion_list_store.write(); + store.process_inclusion_list( + signed_inclusion_list(slot, timely_submitter, dependent_root, 0xaa), + true, + ); + store.process_inclusion_list( + signed_inclusion_list(slot, late_submitter, dependent_root, 0xbb), + false, + ); + drop(store); + + // A validator holding two positions has both bits set. + let bits = harness + .chain + .get_inclusion_list_bits(block_root, slot, false) + .unwrap(); + for (position, validator_index) in committee.iter().enumerate() { + let expected = *validator_index == timely_submitter || *validator_index == late_submitter; + assert_eq!(bits.get(position).unwrap(), expected); + } + + let timely_bits = harness + .chain + .get_inclusion_list_bits(block_root, slot, true) + .unwrap(); + for (position, validator_index) in committee.iter().enumerate() { + assert_eq!( + timely_bits.get(position).unwrap(), + *validator_index == timely_submitter + ); + } + + assert!( + harness + .chain + .is_inclusion_list_bits_inclusive(block_root, slot, &bits, false) + .unwrap() + ); + assert!( + !harness + .chain + .is_inclusion_list_bits_inclusive(block_root, slot, &timely_bits, false) + .unwrap() + ); + + // The spec does not require transaction order to be preserved. + let transactions = harness + .chain + .get_inclusion_list_transactions(block_root, slot, false) + .unwrap(); + assert_eq!(transactions.len(), 2); + assert!(transactions.contains(&transaction(0xaa))); + assert!(transactions.contains(&transaction(0xbb))); + + assert_eq!( + harness + .chain + .get_inclusion_list_transactions(block_root, slot, true) + .unwrap(), + vec![transaction(0xaa)] + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn per_slot_task_prunes_the_store() { + let harness = get_harness(); + let slot = Slot::new(1); + let block_root = harness.head_block_root(); + + let (committee, dependent_root) = harness + .chain + .inclusion_list_committee(block_root, slot) + .unwrap(); + + harness + .chain + .inclusion_list_store + .write() + .process_inclusion_list( + signed_inclusion_list(slot, committee[0], dependent_root, 0xaa), + true, + ); + + // The store retains the two slots behind the current one. + while harness.chain.slot().unwrap() < slot + 2 { + harness.advance_slot(); + } + harness.chain.per_slot_task().await; + assert!( + !harness + .chain + .get_inclusion_list_transactions(block_root, slot, false) + .unwrap() + .is_empty() + ); + + harness.advance_slot(); + harness.chain.per_slot_task().await; + assert!( + harness + .chain + .get_inclusion_list_transactions(block_root, slot, false) + .unwrap() + .is_empty() + ); +} diff --git a/beacon_node/beacon_chain/tests/main.rs b/beacon_node/beacon_chain/tests/main.rs index 26ec9a3c6af..ee3e4752578 100644 --- a/beacon_node/beacon_chain/tests/main.rs +++ b/beacon_node/beacon_chain/tests/main.rs @@ -5,6 +5,7 @@ mod block_verification; mod column_verification; mod envelope_verification; mod events; +mod inclusion_list_store; mod op_verification; mod payload_invalidation; mod prepare_payload; From 73bfb176eec35da9331fc322ef4861e2fd5fe3a6 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 2 Sep 2026 20:02:27 +0300 Subject: [PATCH 27/30] Handle PayloadStatusV2 in the forkchoiceUpdatedV5's call response --- beacon_node/execution_layer/src/engine_api.rs | 2 + .../execution_layer/src/engine_api/http.rs | 47 +++++++++++++++- .../src/engine_api/json_structures.rs | 56 ++++++++++++++++++- .../test_utils/execution_block_generator.rs | 3 + .../execution_layer/src/test_utils/mod.rs | 5 ++ beacon_node/http_api/tests/status_tests.rs | 1 + testing/ef_tests/src/cases/fork_choice.rs | 2 + .../ef_tests/src/cases/gossip_validation.rs | 1 + 8 files changed, 115 insertions(+), 2 deletions(-) diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 6608db835df..827d7e74282 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -119,6 +119,8 @@ pub struct PayloadStatusV1 { pub status: PayloadStatusV1Status, pub latest_valid_hash: Option, pub validation_error: Option, + /// [New in Heze:EIP-7805] Only populated from `engine_forkchoiceUpdatedV5` onwards. + pub inclusion_list_satisfied: Option, } #[derive(Clone, Copy, Debug, PartialEq, Serialize)] diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 15aeb8a818a..a90a20d5be9 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -1178,7 +1178,7 @@ impl HttpJsonRpc { null ]); - let response: JsonForkchoiceUpdatedV1Response = self + let response: JsonForkchoiceUpdatedV2Response = self .rpc_request( ENGINE_FORKCHOICE_UPDATED_V5, params, @@ -1933,6 +1933,48 @@ mod test { null] }), ) + .await + .with_preloaded_responses( + // engine_forkchoiceUpdatedV5 response validation + vec![json!({ + "id": STATIC_ID, + "jsonrpc": JSONRPC_VERSION, + "result": { + "payloadStatus": { + "status": "VALID", + "latestValidHash": HASH_01, + "validationError": null, + "inclusionListSatisfied": true, + }, + "payloadId": "0xa247243752eb10b4" + } + })], + |client| async move { + let response = client + .forkchoice_updated_v5( + ForkchoiceState { + head_block_hash: ExecutionBlockHash::repeat_byte(1), + safe_block_hash: ExecutionBlockHash::repeat_byte(1), + finalized_block_hash: ExecutionBlockHash::zero(), + }, + payload_attributes(), + ) + .await + .unwrap(); + assert_eq!( + response, + ForkchoiceUpdatedResponse { + payload_status: PayloadStatusV1 { + status: PayloadStatusV1Status::Valid, + latest_valid_hash: Some(ExecutionBlockHash::repeat_byte(1)), + validation_error: None, + inclusion_list_satisfied: Some(true), + }, + payload_id: Some(str_to_payload_id("0xa247243752eb10b4")), + } + ); + }, + ) .await; Tester::new(false) @@ -2187,6 +2229,7 @@ mod test { status: PayloadStatusV1Status::Valid, latest_valid_hash: Some(ExecutionBlockHash::zero()), validation_error: Some(String::new()), + inclusion_list_satisfied: None, }, payload_id: Some(str_to_payload_id("0xa247243752eb10b4")), @@ -2326,6 +2369,7 @@ mod test { status: PayloadStatusV1Status::Valid, latest_valid_hash: Some(ExecutionBlockHash::from_str("0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858").unwrap()), validation_error: Some(String::new()), + inclusion_list_satisfied: None, } ); }, @@ -2389,6 +2433,7 @@ mod test { status: PayloadStatusV1Status::Valid, latest_valid_hash: Some(ExecutionBlockHash::zero()), validation_error: Some(String::new()), + inclusion_list_satisfied: None, }, payload_id: None, }); diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 95b836462ed..3bf76b46900 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -1137,6 +1137,15 @@ pub struct JsonPayloadStatusV1 { pub validation_error: Option, } +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JsonPayloadStatusV2 { + pub status: JsonPayloadStatusV1Status, + pub latest_valid_hash: Option, + pub validation_error: Option, + pub inclusion_list_satisfied: Option, +} + impl From for JsonPayloadStatusV1Status { fn from(e: PayloadStatusV1Status) -> Self { match e { @@ -1167,6 +1176,8 @@ impl From for JsonPayloadStatusV1 { status, latest_valid_hash, validation_error, + // Deliberately dropped because the V1 wire format cannot carry it. + inclusion_list_satisfied: _, } = p; Self { @@ -1176,7 +1187,6 @@ impl From for JsonPayloadStatusV1 { } } } - impl From for PayloadStatusV1 { fn from(j: JsonPayloadStatusV1) -> Self { // Use this verbose deconstruction pattern to ensure no field is left unused. @@ -1190,10 +1200,38 @@ impl From for PayloadStatusV1 { status: status.into(), latest_valid_hash, validation_error, + // The V1 wire format carries no inclusion list information. + inclusion_list_satisfied: None, + } + } +} + +impl From for PayloadStatusV1 { + fn from(j: JsonPayloadStatusV2) -> Self { + // Use this verbose deconstruction pattern to ensure no field is left unused. + let JsonPayloadStatusV2 { + status, + latest_valid_hash, + validation_error, + inclusion_list_satisfied, + } = j; + + Self { + status: status.into(), + latest_valid_hash, + validation_error, + inclusion_list_satisfied, } } } +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JsonForkchoiceUpdatedV2Response { + pub payload_status: JsonPayloadStatusV2, + pub payload_id: Option, +} + #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct JsonForkchoiceUpdatedV1Response { @@ -1215,6 +1253,22 @@ impl From for ForkchoiceUpdatedResponse { } } } + +impl From for ForkchoiceUpdatedResponse { + fn from(j: JsonForkchoiceUpdatedV2Response) -> Self { + // Use this verbose deconstruction pattern to ensure no field is left unused. + let JsonForkchoiceUpdatedV2Response { + payload_status: status, + payload_id, + } = j; + + Self { + payload_status: status.into(), + payload_id: payload_id.map(Into::into), + } + } +} + impl From for JsonForkchoiceUpdatedV1Response { fn from(f: ForkchoiceUpdatedResponse) -> Self { // Use this verbose deconstruction pattern to ensure no field is left unused. diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index f92334f7742..e7f85a27a82 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -529,6 +529,7 @@ impl ExecutionBlockGenerator { status: PayloadStatusV1Status::Syncing, latest_valid_hash: None, validation_error: None, + inclusion_list_satisfied: None, }; }; @@ -537,6 +538,7 @@ impl ExecutionBlockGenerator { status: PayloadStatusV1Status::Invalid, latest_valid_hash: Some(parent.block_hash()), validation_error: Some("invalid block number".to_string()), + inclusion_list_satisfied: None, }; } @@ -547,6 +549,7 @@ impl ExecutionBlockGenerator { status: PayloadStatusV1Status::Valid, latest_valid_hash: Some(valid_hash), validation_error: None, + inclusion_list_satisfied: None, } } diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 1b84b08b0a6..4cda98d78b6 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -270,6 +270,7 @@ impl MockServer { status: PayloadStatusV1Status::Valid, latest_valid_hash: None, validation_error: None, + inclusion_list_satisfied: None, } } @@ -285,6 +286,7 @@ impl MockServer { status: PayloadStatusV1Status::Syncing, latest_valid_hash: None, validation_error: None, + inclusion_list_satisfied: None, } } @@ -300,6 +302,7 @@ impl MockServer { status: PayloadStatusV1Status::Invalid, latest_valid_hash: Some(latest_valid_hash), validation_error: Some("static response".into()), + inclusion_list_satisfied: None, } } @@ -317,6 +320,7 @@ impl MockServer { status: PayloadStatusV1Status::InvalidBlockHash, latest_valid_hash: None, validation_error: Some("static response".into()), + inclusion_list_satisfied: None, } } @@ -332,6 +336,7 @@ impl MockServer { status: PayloadStatusV1Status::Invalid, latest_valid_hash: Some(ExecutionBlockHash::zero()), validation_error: Some("static response".into()), + inclusion_list_satisfied: None, } } diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index 8b0d9899ee3..3b574034bf8 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -141,6 +141,7 @@ async fn el_error_on_new_payload() { status: PayloadStatusV1Status::Valid, latest_valid_hash: Some(block_hash), validation_error: None, + inclusion_list_satisfied: None, }, ); harness.process_block_result((block, blobs)).await.unwrap(); diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index c9607f9d274..49b4bef95d1 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -116,6 +116,7 @@ impl From for PayloadStatusV1 { status: status.status.into(), latest_valid_hash: status.latest_valid_hash, validation_error: status.validation_error, + inclusion_list_satisfied: None, } } } @@ -1196,6 +1197,7 @@ impl Tester { status: JsonPayloadStatusV1Status::Valid.into(), latest_valid_hash: Some(block_hash), validation_error: None, + inclusion_list_satisfied: None, }, ); } diff --git a/testing/ef_tests/src/cases/gossip_validation.rs b/testing/ef_tests/src/cases/gossip_validation.rs index 023e9ebbca9..4c2f1555364 100644 --- a/testing/ef_tests/src/cases/gossip_validation.rs +++ b/testing/ef_tests/src/cases/gossip_validation.rs @@ -692,6 +692,7 @@ impl GossipTester { status: PayloadStatusV1Status::Valid, latest_valid_hash: Some(block_hash), validation_error: None, + inclusion_list_satisfied: None, }, ); } From 8ad3cce1090f1e27d82573fd108dd9edced4476c Mon Sep 17 00:00:00 2001 From: conache Date: Sat, 5 Sep 2026 16:38:33 +0300 Subject: [PATCH 28/30] Fix complete_state_advance call the inests around the heze fork boundary --- beacon_node/beacon_chain/tests/prepare_payload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/tests/prepare_payload.rs b/beacon_node/beacon_chain/tests/prepare_payload.rs index 970b21edfd6..faa05656138 100644 --- a/beacon_node/beacon_chain/tests/prepare_payload.rs +++ b/beacon_node/beacon_chain/tests/prepare_payload.rs @@ -734,7 +734,7 @@ async fn prepare_payload_around_heze_boundary(prepare_slot: Slot, heze_fork_epoc let parent_payload_status = cached_head.head_payload_status(); let mut advanced_state = cached_head.snapshot.beacon_state.clone(); - complete_state_advance(&mut advanced_state, None, prepare_slot, &spec).unwrap(); + complete_state_advance(&mut advanced_state, None, prepare_slot, None, &spec).unwrap(); // Call `prepare_beacon_proposer` for the next slot. // This sends a fcU with the payload attributes to the execution layer From 5a56dbcaf67990660511948443d0bc6c1c02b7b5 Mon Sep 17 00:00:00 2001 From: rahulbarman Date: Wed, 16 Sep 2026 09:04:17 +0530 Subject: [PATCH 29/30] Add MAX_SIGNED_INCLUSION_LIST_SIZE to the Heze preset --- consensus/types/presets/gnosis/heze.yaml | 5 +++++ consensus/types/presets/mainnet/heze.yaml | 5 +++++ consensus/types/presets/minimal/heze.yaml | 5 +++++ consensus/types/src/core/eth_spec.rs | 18 ++++++++++++++---- consensus/types/src/core/preset.rs | 3 +++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/consensus/types/presets/gnosis/heze.yaml b/consensus/types/presets/gnosis/heze.yaml index 8aa06a2a519..eb4f5c75ebb 100644 --- a/consensus/types/presets/gnosis/heze.yaml +++ b/consensus/types/presets/gnosis/heze.yaml @@ -4,3 +4,8 @@ # --------------------------------------------------------------- # 2**4 (= 16) validators INCLUSION_LIST_COMMITTEE_SIZE: 16 + +# Type-specific SSZ bounds +# --------------------------------------------------------------- +# 41,112 bytes, ~40 KiB +MAX_SIGNED_INCLUSION_LIST_SIZE: 41112 diff --git a/consensus/types/presets/mainnet/heze.yaml b/consensus/types/presets/mainnet/heze.yaml index be038fbcc20..c0d9a4dd382 100644 --- a/consensus/types/presets/mainnet/heze.yaml +++ b/consensus/types/presets/mainnet/heze.yaml @@ -4,3 +4,8 @@ # --------------------------------------------------------------- # 2**4 (= 16) validators INCLUSION_LIST_COMMITTEE_SIZE: 16 + +# Type-specific SSZ bounds +# --------------------------------------------------------------- +# 41,112 bytes, ~40 KiB +MAX_SIGNED_INCLUSION_LIST_SIZE: 41112 diff --git a/consensus/types/presets/minimal/heze.yaml b/consensus/types/presets/minimal/heze.yaml index a36f5d7d35d..d31fa7f3a29 100644 --- a/consensus/types/presets/minimal/heze.yaml +++ b/consensus/types/presets/minimal/heze.yaml @@ -4,3 +4,8 @@ # --------------------------------------------------------------- # 2**4 (= 16) validators INCLUSION_LIST_COMMITTEE_SIZE: 16 + +# Type-specific SSZ bounds +# --------------------------------------------------------------- +# 41,112 bytes, ~40 KiB +MAX_SIGNED_INCLUSION_LIST_SIZE: 41112 diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index 888d9f50a16..0e45bbb895d 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -466,10 +466,8 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + /// Returns the `MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE` constant for this specification. fn max_signed_execution_payload_bid_size() -> usize; - /// Returns the `MAX_SIGNED_INCLUSION_LIST_SIZE` constant. - fn max_signed_inclusion_list_size() -> usize { - 41112 - } + /// Returns the `MAX_SIGNED_INCLUSION_LIST_SIZE` constant for this specification. + fn max_signed_inclusion_list_size() -> usize; /// Returns the `PAYLOAD_TIMELY_THRESHOLD` constant (PTC_SIZE / 2). fn payload_timely_threshold() -> usize { @@ -590,6 +588,10 @@ impl EthSpec for MainnetEthSpec { fn max_signed_execution_payload_bid_size() -> usize { 196932 } + + fn max_signed_inclusion_list_size() -> usize { + 41112 + } } /// Ethereum Foundation minimal spec, as defined in the eth2.0-specs repo. @@ -690,6 +692,10 @@ impl EthSpec for MinimalEthSpec { fn max_signed_execution_payload_bid_size() -> usize { 196932 } + + fn max_signed_inclusion_list_size() -> usize { + 41112 + } } /// Gnosis Beacon Chain specifications. @@ -787,6 +793,10 @@ impl EthSpec for GnosisEthSpec { fn max_signed_execution_payload_bid_size() -> usize { 196932 } + + fn max_signed_inclusion_list_size() -> usize { + 41112 + } } #[cfg(test)] diff --git a/consensus/types/src/core/preset.rs b/consensus/types/src/core/preset.rs index 43fa650e13c..39d4ea88117 100644 --- a/consensus/types/src/core/preset.rs +++ b/consensus/types/src/core/preset.rs @@ -379,12 +379,15 @@ impl GloasPreset { pub struct HezePreset { #[serde(with = "serde_utils::quoted_u64")] pub inclusion_list_committee_size: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub max_signed_inclusion_list_size: u64, } impl HezePreset { pub fn from_chain_spec(_spec: &ChainSpec) -> Self { Self { inclusion_list_committee_size: E::inclusion_list_committee_size() as u64, + max_signed_inclusion_list_size: E::max_signed_inclusion_list_size() as u64, } } } From 19e84f08b492fb684b078eca7992a936c2a4418b Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 30 Sep 2026 15:20:14 +0300 Subject: [PATCH 30/30] Remove redundant clone --- beacon_node/http_api/src/validator/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index bb093c3a3b4..e7481eda149 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1533,7 +1533,7 @@ 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(not_while_syncing_filter) .and(task_spawner_filter) .and(chain_filter) .and(network_tx_filter)