diff --git a/Cargo.lock b/Cargo.lock index 76426650d4a..ebe56ddf814 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4386,6 +4386,7 @@ dependencies = [ "serde", "serde_json", "slot_clock", + "ssz_types", "state_processing", "store", "sysinfo", diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 655db71ea18..49e1778e616 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -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; @@ -650,13 +650,31 @@ 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, + // 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::>, + }, + }), }; 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 dfee0160eb8..f1285fa161d 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -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 = (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 4822d1c8fae..40a47c44c47 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -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 = self .rpc_request( ENGINE_GET_PAYLOAD_V6, @@ -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 @@ -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 diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 944757809a0..161d231bba7 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -227,8 +227,6 @@ impl From> for BlockProposalContentsGloas } } -// TODO(heze): add a `BlockProposalContentsHeze` here once Heze block production is wired up. - pub enum BlockProposalContents> { Payload { payload: Payload, @@ -954,8 +952,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 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 e1dee7d85ed..9e7511a9e51 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 @@ -871,6 +871,13 @@ impl ExecutionBlockGenerator { .push(ProgressiveVariableList::::new(tx.into())); } } + ExecutionPayload::Heze(payload) => { + for tx in Vec::from(transactions) { + payload + .transactions + .push(ProgressiveVariableList::::new(tx.into())); + } + } _ => { for tx in Vec::from(transactions) { execution_payload diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 62b316beda4..1195f12ce4a 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -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 } diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index a020c633c82..471a8883327 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -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, }; @@ -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; @@ -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::::new_with_initializer_and_mutator( Some(spec.clone()), @@ -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::::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::().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::::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::::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), + } +}