Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
77c41f9
Add method to produce ILs for the current slot
conache Jul 16, 2026
9a5256a
Complete http api endpoint support
conache Jul 20, 2026
22623d9
Add pre-fork test
conache Jul 20, 2026
d971140
Tweak todo
conache Jul 20, 2026
932885f
Add support for Heze block production
conache Jul 22, 2026
750a50c
Use latest fork for state_by_root_pruned_from_fork_choice
conache Jul 22, 2026
5763128
Add IL fetching handler tests
conache Jul 22, 2026
abac19c
Add support for Heze block production
conache Jul 22, 2026
4b5074f
Use latest fork for state_by_root_pruned_from_fork_choice
conache Jul 22, 2026
602508c
Merge branch 'unstable' into heze-block-production
conache Jul 29, 2026
d676427
Merge branch 'unstable' into heze-block-production
conache Jul 30, 2026
6fa22e9
Merge branch 'unstable' into heze-block-production
conache Aug 1, 2026
b89aa8d
Merge branch 'unstable' into heze-block-production
conache Aug 3, 2026
d473556
Fix formatting
conache Aug 3, 2026
2476042
Merge branch 'unstable' of https://github.com/sigp/lighthouse into he…
conache Aug 4, 2026
5acb5d3
Merge branch 'support-get-il-v1' into add-il-fetch-endpoint-to-beacon…
conache Aug 4, 2026
fdda8a1
Merge branch 'add-il-fetch-endpoint-to-beacon-api' into followup-add-…
conache Aug 4, 2026
b9d0fdc
Tests cleanup
conache Aug 4, 2026
2d966e3
Merge branch 'heze-block-production' into followup-add-il-fetching-en…
conache Aug 4, 2026
ab96ea5
Merge branch 'unstable' into heze-block-production
conache Aug 5, 2026
4a9a658
Add method to produce ILs for the current slot
conache Jul 16, 2026
e94dd02
Complete http api endpoint support
conache Jul 20, 2026
1ef78e7
Add pre-fork test
conache Jul 20, 2026
5503f08
Tweak todo
conache Jul 20, 2026
fd4782e
Merge branch 'unstable' into heze-block-production
conache Aug 6, 2026
e6641b8
Use ProgressiveTransactions in the InclusionListTransactions struct
conache Aug 6, 2026
207a4a3
Merge branch 'add-il-fetch-endpoint-to-beacon-api' into followup-add-…
conache Aug 6, 2026
49870eb
Merge branch 'unstable' into heze-block-production
conache Aug 8, 2026
0b0cdf7
Merge branch 'unstable' into heze-block-production
conache Aug 10, 2026
d8a3403
Merge branch 'unstable' into heze-block-production
conache Aug 10, 2026
db57fb7
Add clearer comments
conache Aug 10, 2026
fd951f1
Merge branch 'unstable' into heze-block-production
conache Aug 11, 2026
02250f9
Merge branch 'unstable' into heze-block-production
conache Aug 12, 2026
5cebdd7
Merge branch 'heze-block-production' into followup-add-il-fetching-en…
conache Aug 12, 2026
b3d282b
Merge branch 'add-il-fetch-endpoint-to-beacon-api' into followup-add-…
conache Aug 12, 2026
8af56d9
Merge branch 'add-il-fetch-endpoint-to-beacon-api' into followup-add-…
conache Aug 12, 2026
29d1865
Merge branch 'add-il-fetch-endpoint-to-beacon-api' into followup-add-…
conache Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 32 additions & 14 deletions beacon_node/beacon_chain/src/block_production/gloas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ use tree_hash::TreeHash;
use types::consts::gloas::BUILDER_INDEX_SELF_BUILD;
use types::{
Address, Attestation, AttestationGloas, AttesterSlashing, AttesterSlashingGloas, BeaconBlock,
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,
BeaconBlockBodyGloas, BeaconBlockBodyHeze, BeaconBlockGloas, BeaconBlockHeze, 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,
};

use crate::payload_bid_verification::payload_bid_cache::BidParent;
Expand Down Expand Up @@ -650,13 +650,31 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
_phantom: PhantomData::<FullPayload<T::EthSpec>>,
},
}),
// 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,
// The operation list lengths are bounded by the op pool packing limits above.
proposer_slashings: ProgressiveVariableList::from_iter(proposer_slashings),
attester_slashings: ProgressiveVariableList::from_iter(attester_slashings),
attestations: ProgressiveVariableList::from_iter(attestations),
deposits: ProgressiveVariableList::from_iter(deposits),
voluntary_exits: ProgressiveVariableList::from_iter(voluntary_exits),
sync_aggregate,
bls_to_execution_changes: ProgressiveVariableList::from_iter(
bls_to_execution_changes,
),
parent_execution_requests,
signed_execution_payload_bid,
payload_attestations: ProgressiveVariableList::from_iter(payload_attestations),
_phantom: PhantomData::<FullPayload<T::EthSpec>>,
},
}),
};

let signed_beacon_block = SignedBeaconBlock::from_block(
Expand Down
52 changes: 52 additions & 0 deletions beacon_node/beacon_chain/tests/store_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,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 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<Slot> = (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() {
Expand Down
12 changes: 4 additions & 8 deletions beacon_node/execution_layer/src/engine_api/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,9 @@ impl HttpJsonRpc {
let params = json!([JsonPayloadIdRequest::from(payload_id)]);

match fork_name {
ForkName::Gloas => {
// 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<E> = self
.rpc_request(
ENGINE_GET_PAYLOAD_V6,
Expand All @@ -1093,7 +1095,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
Expand Down Expand Up @@ -1465,18 +1466,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
Expand Down
4 changes: 0 additions & 4 deletions beacon_node/execution_layer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,6 @@ impl<E: EthSpec> From<GetPayloadResponseGloas<E>> for BlockProposalContentsGloas
}
}

// TODO(heze): add a `BlockProposalContentsHeze` here once Heze block production is wired up.

pub enum BlockProposalContents<E: EthSpec, Payload: AbstractExecPayload<E>> {
Payload {
payload: Payload,
Expand Down Expand Up @@ -954,8 +952,6 @@ impl<E: EthSpec> ExecutionLayer<E> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,13 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> {
.push(ProgressiveVariableList::<u8>::new(tx.into()));
}
}
ExecutionPayload::Heze(payload) => {
for tx in Vec::from(transactions) {
payload
.transactions
.push(ProgressiveVariableList::<u8>::new(tx.into()));
}
}
_ => {
for tx in Vec::from(transactions) {
execution_payload
Expand Down
1 change: 1 addition & 0 deletions beacon_node/http_api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ sensitive_url = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
slot_clock = { workspace = true }
ssz_types = { workspace = true }
state_processing = { workspace = true }
store = { workspace = true }
sysinfo = { workspace = true }
Expand Down
117 changes: 113 additions & 4 deletions beacon_node/http_api/tests/interactive_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ use beacon_chain::{
use beacon_processor::{Work, WorkEvent, work_reprocessing_queue::ReprocessQueueMessage};
use eth2::types::ProduceBlockV3Response;
use eth2::types::{DepositContractData, StateId};
use execution_layer::{ForkchoiceState, PayloadAttributes};
use execution_layer::{ForkchoiceState, PayloadAttributes, test_utils::static_valid_tx};
use fixed_bytes::FixedBytesExtended;
use http_api::test_utils::InteractiveTester;
use parking_lot::Mutex;
use slot_clock::SlotClock;
use ssz_types::ProgressiveVariableList;
use state_processing::{
per_block_processing::get_expected_withdrawals, state_advance::complete_state_advance,
};
Expand All @@ -23,7 +24,7 @@ use std::sync::Arc;
use std::time::Duration;
use types::{
Address, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec,
MinimalEthSpec, ProposerPreparationData, Slot,
MinimalEthSpec, ProgressiveTransactions, ProposerPreparationData, Slot,
};

type E = MainnetEthSpec;
Expand Down Expand Up @@ -61,8 +62,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::<E>::new_with_initializer_and_mutator(
Some(spec.clone()),
Expand Down Expand Up @@ -1387,3 +1387,112 @@ async fn lighthouse_custody_info() {
info.custody_group_count as usize
);
}

// Test that the validator inclusion list endpoint returns the transactions provided by the EL for
// the current slot.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_validator_inclusion_list() {
let fork_name = fork_name_from_env().unwrap_or_else(ForkName::latest);
if !fork_name.heze_enabled() {
return;
}

let validator_count = 64;
let spec = fork_name.make_genesis_spec(E::default_spec());
let tester = InteractiveTester::<E>::new(Some(spec), validator_count).await;
let client = &tester.client;
let harness = &tester.harness;

// Build a short chain so the head references a known execution payload.
harness.advance_slot();
harness
.extend_chain(
E::slots_per_epoch() as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;

// Configure the IL transactions returned by the mock EL.
let transactions = ProgressiveTransactions::new(vec![ProgressiveVariableList::new(
static_valid_tx::<E>().unwrap().to_vec(),
)]);
let mock_el = harness.mock_execution_layer.as_ref().unwrap();
mock_el
.server
.execution_block_generator()
.set_inclusion_list(transactions.clone());

let slot = harness.chain.slot().unwrap();
let response = client.get_validator_inclusion_list(slot).await.unwrap();
assert_eq!(response.data.transactions, transactions);
}

// Test that the validator inclusion list endpoint rejects requests for non-current slots.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_validator_inclusion_list_invalid_slot() {
let fork_name = fork_name_from_env().unwrap_or_else(ForkName::latest);
if !fork_name.heze_enabled() {
return;
}

let validator_count = 64;
let spec = fork_name.make_genesis_spec(E::default_spec());
let tester = InteractiveTester::<E>::new(Some(spec), validator_count).await;
let client = &tester.client;
let harness = &tester.harness;

harness.advance_slot();
harness
.extend_chain(
E::slots_per_epoch() as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;

// Inclusion lists are only produced for the current slot: past and future slots are rejected.
let current_slot = harness.chain.slot().unwrap();
for slot in [current_slot - 1, current_slot + 1] {
match client.get_validator_inclusion_list(slot).await {
Ok(response) => panic!("query for slot {slot} should fail, got: {response:?}"),
Err(e) => assert_eq!(e.status().unwrap(), 400),
}
}
}

// Test that the validator inclusion list endpoint returns a server error when the EL fails to
// provide the inclusion list.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_validator_inclusion_list_el_failure() {
let fork_name = fork_name_from_env().unwrap_or_else(ForkName::latest);
if !fork_name.heze_enabled() {
return;
}

let validator_count = 64;
let spec = fork_name.make_genesis_spec(E::default_spec());
let tester = InteractiveTester::<E>::new(Some(spec), validator_count).await;
let client = &tester.client;
let harness = &tester.harness;

harness.advance_slot();
harness
.extend_chain(
E::slots_per_epoch() as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;

// Drop the mock EL's blocks so the head block hash is unknown to it,
// making the getInclusionList call fail
let mock_el = harness.mock_execution_layer.as_ref().unwrap();
mock_el.server.drop_all_blocks();

let slot = harness.chain.slot().unwrap();
match client.get_validator_inclusion_list(slot).await {
Ok(response) => panic!("query should fail when the EL errors, got: {response:?}"),
Err(e) => assert_eq!(e.status().unwrap(), 500),
}
}
Loading