Skip to content
20 changes: 20 additions & 0 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7604,6 +7604,26 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.map_err(Into::into)
}

/// Provides the timely inclusion list transactions for a payload built on `parent_root` at
/// `slot + 1`, falling back to an empty list if they can't be read.
pub fn payload_inclusion_list_transactions(
&self,
parent_root: Hash256,
slot: Slot,
) -> ProgressiveTransactions {
self.get_inclusion_list_transactions(parent_root, slot, true)
.and_then(|transactions| ProgressiveTransactions::new(transactions).map_err(Into::into))
.unwrap_or_else(|e| {
warn!(
?parent_root,
%slot,
error = ?e,
"Failed to read inclusion lists, verifying payload without them"
);
ProgressiveTransactions::empty()
})
}

/// 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::sync::Arc;

use execution_layer::{NewPayloadRequest, NewPayloadRequestGloas};
use execution_layer::{NewPayloadRequest, NewPayloadRequestGloas, NewPayloadRequestHeze};
use fork_choice::PayloadVerificationStatus;
use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash;
use tracing::warn;
Expand Down Expand Up @@ -31,7 +31,8 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> {

match notify_execution_layer {
NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => {
let new_payload_request = Self::build_new_payload_request(&envelope, &block)?;
let new_payload_request =
Self::build_new_payload_request(&chain, &envelope, &block)?;
// TODO(gloas): check and test RLP block hash calculation post-Gloas
if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() {
warn!(
Expand Down Expand Up @@ -68,12 +69,14 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> {
if let Some(precomputed_status) = self.payload_verification_status {
Ok(precomputed_status)
} else {
let request = Self::build_new_payload_request(&self.envelope, &self.block)?;
let request =
Self::build_new_payload_request(&self.chain, &self.envelope, &self.block)?;
notify_new_payload(&self.chain, self.envelope.slot(), request).await
}
}

fn build_new_payload_request<'a>(
chain: &BeaconChain<T>,
envelope: &'a SignedExecutionPayloadEnvelope<T::EthSpec>,
block: &'a SignedBeaconBlock<T::EthSpec>,
) -> Result<NewPayloadRequest<'a, T::EthSpec>, PayloadVerificationError> {
Expand All @@ -90,11 +93,29 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> {
.map(kzg_commitment_to_versioned_hash)
.collect();

Ok(NewPayloadRequest::Gloas(NewPayloadRequestGloas {
execution_payload: &envelope.message.payload,
versioned_hashes,
parent_beacon_block_root: envelope.message.parent_beacon_block_root,
execution_requests: &envelope.message.execution_requests,
}))
if chain
.spec
.fork_name_at_slot::<T::EthSpec>(block.slot())
.heze_enabled()
{
let inclusion_list_transactions = chain.payload_inclusion_list_transactions(
block.parent_root(),
block.slot().saturating_sub(1_u64),
);
Ok(NewPayloadRequest::Heze(NewPayloadRequestHeze {
execution_payload: &envelope.message.payload,
versioned_hashes,
parent_beacon_block_root: envelope.message.parent_beacon_block_root,
execution_requests: &envelope.message.execution_requests,
inclusion_list_transactions,
}))
} else {
Ok(NewPayloadRequest::Gloas(NewPayloadRequestGloas {
execution_payload: &envelope.message.payload,
versioned_hashes,
parent_beacon_block_root: envelope.message.parent_beacon_block_root,
execution_requests: &envelope.message.execution_requests,
}))
}
}
}
31 changes: 24 additions & 7 deletions beacon_node/beacon_chain/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use bls::{
use eth2::types::{GraffitiPolicy, SignedBlockContentsTuple};
use execution_layer::test_utils::generate_genesis_header;
use execution_layer::{
ExecutionLayer, NewPayloadRequest, NewPayloadRequestGloas,
ExecutionLayer, NewPayloadRequest, NewPayloadRequestGloas, NewPayloadRequestHeze,
auth::JwtKey,
test_utils::{DEFAULT_JWT_SECRET, ExecutionBlockGenerator, MockBuilder, MockExecutionLayer},
};
Expand Down Expand Up @@ -3165,12 +3165,29 @@ where
.map(kzg_commitment_to_versioned_hash)
.collect();

let request = NewPayloadRequest::Gloas(NewPayloadRequestGloas {
execution_payload: &signed_envelope.message.payload,
versioned_hashes,
parent_beacon_block_root: block.message().parent_root(),
execution_requests: &signed_envelope.message.execution_requests,
});
let request = if self
.spec
.fork_name_at_slot::<E>(block.slot())
.heze_enabled()
{
NewPayloadRequest::Heze(NewPayloadRequestHeze {
execution_payload: &signed_envelope.message.payload,
versioned_hashes,
parent_beacon_block_root: block.message().parent_root(),
execution_requests: &signed_envelope.message.execution_requests,
inclusion_list_transactions: self.chain.payload_inclusion_list_transactions(
block.parent_root(),
block.slot().saturating_sub(1_u64),
),
})
} else {
NewPayloadRequest::Gloas(NewPayloadRequestGloas {
execution_payload: &signed_envelope.message.payload,
versioned_hashes,
parent_beacon_block_root: block.message().parent_root(),
execution_requests: &signed_envelope.message.execution_requests,
})
};

self.chain
.execution_layer
Expand Down
89 changes: 86 additions & 3 deletions beacon_node/beacon_chain/tests/envelope_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ use beacon_chain::payload_envelope_verification::{EnvelopeError, EnvelopeSource}
use beacon_chain::test_utils::{
BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec,
};
use bls::PublicKeyBytes;
use bls::{PublicKeyBytes, Signature};
use eth2::types::EventKind;
use execution_layer::http::ENGINE_NEW_PAYLOAD_V6;
use parking_lot::Mutex;
use proto_array::ExecutionStatus;
use ssz_types::ProgressiveVariableList;
use std::sync::Arc;
use types::{
Address, BlockImportSource, Epoch, ExecPayload, ForkName, Hash256, Slot, Spec,
WithdrawalRequest,
Address, BlockImportSource, Epoch, EthSpec, ExecPayload, ForkName, Hash256, InclusionList,
ProgressiveTransactions, SignedInclusionList, Slot, Spec, WithdrawalRequest,
};

type E = Spec;
Expand Down Expand Up @@ -735,3 +738,83 @@ fn is_valid_and_post_bellatrix(status: ExecutionStatus) -> bool {
fn is_optimistic(status: ExecutionStatus) -> bool {
matches!(status, ExecutionStatus::Optimistic(_))
}

/// A Heze payload is sent over `engine_newPayloadV6` with the timely inclusion lists from the
/// previous slot, which is in the previous epoch here.
#[tokio::test]
async fn heze_payload_is_sent_with_the_timely_inclusion_lists() {
let mut spec = test_spec::<E>();
if !spec.fork_name_at_slot::<E>(Slot::new(0)).gloas_enabled() {
return;
}
spec.heze_fork_epoch = Some(Epoch::new(1));
let harness = BeaconChainHarness::builder(E::default())
.spec(Arc::new(spec))
.deterministic_keypairs(64)
.fresh_ephemeral_store()
.mock_execution_layer()
.build();

let slot = Epoch::new(2).start_slot(E::slots_per_epoch());
let inclusion_list_slot = slot - 1;
harness.extend_to_slot(inclusion_list_slot).await;

let (committee, dependent_root) = harness
.chain
.inclusion_list_committee(harness.head_block_root(), inclusion_list_slot)
.unwrap();
let timely_submitter = committee[0];
let late_submitter = *committee
.iter()
.find(|index| **index != timely_submitter)
.unwrap();
for (validator_index, tx_byte, is_timely) in [
(timely_submitter, 0xaa, true),
(late_submitter, 0xbb, false),
] {
harness
.chain
.inclusion_list_store
.write()
.process_inclusion_list(
SignedInclusionList {
message: InclusionList {
slot: inclusion_list_slot,
validator_index,
dependent_root,
transactions: ProgressiveTransactions::new(vec![
ProgressiveVariableList::new(vec![tx_byte]).unwrap(),
])
.unwrap(),
},
signature: Signature::empty(),
},
is_timely,
);
}

let new_payload_calls = Arc::new(Mutex::new(vec![]));
let new_payload_calls_inner = new_payload_calls.clone();
harness
.mock_execution_layer
.as_ref()
.unwrap()
.server
.ctx
.hook
.lock()
.set_new_payload_hook(Box::new(move |method, params| {
new_payload_calls_inner
.lock()
.push((method.to_string(), params.clone()));
}));

import_block_and_envelope(&harness, slot).await;

let new_payload_calls = new_payload_calls.lock();
let [(method, params)] = new_payload_calls.as_slice() else {
panic!("expected one newPayload call, got {new_payload_calls:?}");
};
assert_eq!(method, ENGINE_NEW_PAYLOAD_V6);
assert_eq!(params[4], serde_json::json!(["0xaa"]));
}
2 changes: 1 addition & 1 deletion beacon_node/execution_layer/src/engine_api/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@ impl HttpJsonRpc {
new_payload_request_heze: NewPayloadRequestHeze<'_, E>,
) -> Result<PayloadStatusV1, Error> {
let params = json!([
JsonExecutionPayload::Heze(
JsonExecutionPayload::Gloas(
new_payload_request_heze
.execution_payload
.clone()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ use types::{
};
use types::{
ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb,
ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionPayloadHeze,
ExecutionRequestsElectra, ExecutionRequestsGloas, ExecutionRequestsRef,
ProgressiveTransactions,
ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionRequestsElectra,
ExecutionRequestsGloas, ExecutionRequestsRef, ProgressiveTransactions,
};

#[superstruct(
Expand Down Expand Up @@ -43,10 +42,8 @@ pub struct NewPayloadRequest<'block, E: EthSpec> {
pub execution_payload: &'block ExecutionPayloadElectra<E>,
#[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))]
pub execution_payload: &'block ExecutionPayloadFulu<E>,
#[superstruct(only(Gloas), partial_getter(rename = "execution_payload_gloas"))]
#[superstruct(only(Gloas, Heze), partial_getter(rename = "execution_payload_gloas"))]
pub execution_payload: &'block ExecutionPayloadGloas<E>,
#[superstruct(only(Heze), partial_getter(rename = "execution_payload_heze"))]
pub execution_payload: &'block ExecutionPayloadHeze<E>,
#[superstruct(only(Deneb, Electra, Fulu, Gloas, Heze))]
pub versioned_hashes: Vec<VersionedHash>,
#[superstruct(only(Deneb, Electra, Fulu, Gloas, Heze))]
Expand Down Expand Up @@ -83,7 +80,8 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> {
Self::Electra(request) => ExecutionPayloadRef::Electra(request.execution_payload),
Self::Fulu(request) => ExecutionPayloadRef::Fulu(request.execution_payload),
Self::Gloas(request) => ExecutionPayloadRef::Gloas(request.execution_payload),
Self::Heze(request) => ExecutionPayloadRef::Heze(request.execution_payload),
// The envelope payload is an `ExecutionPayloadGloas` for both Gloas and Heze.
Self::Heze(request) => ExecutionPayloadRef::Gloas(request.execution_payload),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think it's worth a small comment here, to clarify why we're doing this.
Something like "the envelope payload is an ExecutionPayloadGloas for both Gloas and Heze"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, thanks!

}
}

Expand Down
5 changes: 2 additions & 3 deletions beacon_node/execution_layer/src/test_utils/handle_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pub async fn handle_rpc<E: EthSpec>(
| ENGINE_NEW_PAYLOAD_V4
| ENGINE_NEW_PAYLOAD_V5
| ENGINE_NEW_PAYLOAD_V6 => {
ctx.hook.lock().on_new_payload(method, params);
let request = match method {
ENGINE_NEW_PAYLOAD_V1 => JsonExecutionPayload::Bellatrix(
get_param::<JsonExecutionPayloadBellatrix<E>>(params, 0)
Expand Down Expand Up @@ -209,9 +210,7 @@ pub async fn handle_rpc<E: EthSpec>(
}
}
ForkName::Heze => {
// TODO(heze): drop V5 here once `NewPayloadRequest::Heze` is constructed. The
// payload notifier still builds the Gloas request, so Heze blocks send V5.
if method != ENGINE_NEW_PAYLOAD_V5 && method != ENGINE_NEW_PAYLOAD_V6 {
if method != ENGINE_NEW_PAYLOAD_V6 {
return Err((
format!("{} called after Heze fork!", method),
GENERIC_ERROR_CODE,
Expand Down
14 changes: 14 additions & 0 deletions beacon_node/execution_layer/src/test_utils/hook.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::json_structures::*;
use serde_json::Value as JsonValue;

type ForkChoiceUpdatedHook = dyn Fn(
JsonForkchoiceStateV1,
Expand All @@ -7,9 +8,12 @@ type ForkChoiceUpdatedHook = dyn Fn(
+ Send
+ Sync;

type NewPayloadHook = dyn Fn(&str, &JsonValue) + Send + Sync;

#[derive(Default)]
pub struct Hook {
forkchoice_updated: Option<Box<ForkChoiceUpdatedHook>>,
new_payload: Option<Box<NewPayloadHook>>,
}

impl Hook {
Expand All @@ -24,4 +28,14 @@ impl Hook {
pub fn set_forkchoice_updated_hook(&mut self, f: Box<ForkChoiceUpdatedHook>) {
self.forkchoice_updated = Some(f);
}

pub fn on_new_payload(&self, method: &str, params: &JsonValue) {
if let Some(f) = self.new_payload.as_ref() {
f(method, params)
}
}

pub fn set_new_payload_hook(&mut self, f: Box<NewPayloadHook>) {
self.new_payload = Some(f);
}
}