mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-03 00:31:50 +00:00
Merge branch 'gloas-block-and-bid-production' into gloas-devnet-0
This commit is contained in:
@@ -61,8 +61,9 @@ use tracing::{debug, error};
|
||||
use tree_hash::TreeHash;
|
||||
use types::{
|
||||
Attestation, AttestationData, AttestationRef, BeaconCommittee,
|
||||
BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256,
|
||||
IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId,
|
||||
BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, ForkName,
|
||||
Hash256, IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot,
|
||||
SubnetId,
|
||||
};
|
||||
|
||||
pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations};
|
||||
@@ -160,6 +161,12 @@ pub enum Error {
|
||||
///
|
||||
/// The peer has sent an invalid message.
|
||||
CommitteeIndexNonZero(usize),
|
||||
/// The validator index is set to an invalid value after Gloas.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The peer has sent an invalid message.
|
||||
CommitteeIndexInvalid,
|
||||
/// The `attestation.data.beacon_block_root` block is unknown.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
@@ -550,8 +557,12 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> {
|
||||
}
|
||||
.tree_hash_root();
|
||||
|
||||
let fork_name = chain
|
||||
.spec
|
||||
.fork_name_at_slot::<T::EthSpec>(attestation.data().slot);
|
||||
|
||||
// [New in Electra:EIP7549]
|
||||
verify_committee_index(attestation)?;
|
||||
verify_committee_index(attestation, fork_name)?;
|
||||
|
||||
if chain
|
||||
.observed_attestations
|
||||
@@ -595,6 +606,17 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> {
|
||||
// attestation and do not delay consideration for later.
|
||||
let head_block = verify_head_block_is_known(chain, attestation.data(), None)?;
|
||||
|
||||
// [New in Gloas]: If the attested block is from the same slot as the attestation,
|
||||
// index must be 0.
|
||||
if fork_name.gloas_enabled()
|
||||
&& head_block.slot == attestation.data().slot
|
||||
&& attestation.data().index != 0
|
||||
{
|
||||
return Err(Error::CommitteeIndexNonZero(
|
||||
attestation.data().index as usize,
|
||||
));
|
||||
}
|
||||
|
||||
// Check the attestation target root is consistent with the head root.
|
||||
//
|
||||
// This check is not in the specification, however we guard against it since it opens us up
|
||||
@@ -871,7 +893,12 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> {
|
||||
let fork_name = chain
|
||||
.spec
|
||||
.fork_name_at_slot::<T::EthSpec>(attestation.data.slot);
|
||||
if fork_name.electra_enabled() {
|
||||
if fork_name.gloas_enabled() {
|
||||
// [New in Gloas]
|
||||
if attestation.data.index >= 2 {
|
||||
return Err(Error::CommitteeIndexInvalid);
|
||||
}
|
||||
} else if fork_name.electra_enabled() {
|
||||
// [New in Electra:EIP7549]
|
||||
if attestation.data.index != 0 {
|
||||
return Err(Error::CommitteeIndexNonZero(
|
||||
@@ -890,6 +917,17 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> {
|
||||
chain.config.import_max_skip_slots,
|
||||
)?;
|
||||
|
||||
// [New in Gloas]: If the attested block is from the same slot as the attestation,
|
||||
// index must be 0.
|
||||
if fork_name.gloas_enabled()
|
||||
&& head_block.slot == attestation.data.slot
|
||||
&& attestation.data.index != 0
|
||||
{
|
||||
return Err(Error::CommitteeIndexNonZero(
|
||||
attestation.data.index as usize,
|
||||
));
|
||||
}
|
||||
|
||||
// Check the attestation target root is consistent with the head root.
|
||||
verify_attestation_target_root::<T::EthSpec>(&head_block, &attestation.data)?;
|
||||
|
||||
@@ -1404,7 +1442,10 @@ pub fn verify_signed_aggregate_signatures<T: BeaconChainTypes>(
|
||||
|
||||
/// Verify that the `attestation` committee index is properly set for the attestation's fork.
|
||||
/// This function will only apply verification post-Electra.
|
||||
pub fn verify_committee_index<E: EthSpec>(attestation: AttestationRef<E>) -> Result<(), Error> {
|
||||
pub fn verify_committee_index<E: EthSpec>(
|
||||
attestation: AttestationRef<E>,
|
||||
fork_name: ForkName,
|
||||
) -> Result<(), Error> {
|
||||
if let Ok(committee_bits) = attestation.committee_bits() {
|
||||
// Check to ensure that the attestation is for a single committee.
|
||||
let num_committee_bits = get_committee_indices::<E>(committee_bits);
|
||||
@@ -1414,11 +1455,18 @@ pub fn verify_committee_index<E: EthSpec>(attestation: AttestationRef<E>) -> Res
|
||||
));
|
||||
}
|
||||
|
||||
// Ensure the attestation index is set to zero post Electra.
|
||||
if attestation.data().index != 0 {
|
||||
return Err(Error::CommitteeIndexNonZero(
|
||||
attestation.data().index as usize,
|
||||
));
|
||||
// Ensure the attestation index is valid for the fork.
|
||||
let index = attestation.data().index;
|
||||
if fork_name.gloas_enabled() {
|
||||
// [New in Gloas]: index must be < 2.
|
||||
if index >= 2 {
|
||||
return Err(Error::CommitteeIndexInvalid);
|
||||
}
|
||||
} else {
|
||||
// [New in Electra:EIP7549]: index must be 0.
|
||||
if index != 0 {
|
||||
return Err(Error::CommitteeIndexNonZero(index as usize));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -3,33 +3,37 @@ use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bls::Signature;
|
||||
use execution_layer::{BlockProposalContentsType, BuilderParams};
|
||||
use execution_layer::{
|
||||
BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters,
|
||||
};
|
||||
use operation_pool::CompactAttestationRef;
|
||||
use ssz::Encode;
|
||||
use state_processing::common::get_attesting_indices_from_state;
|
||||
use state_processing::envelope_processing::{VerifyStateRoot, process_execution_payload_envelope};
|
||||
use state_processing::epoch_cache::initialize_epoch_cache;
|
||||
use state_processing::per_block_processing::verify_attestation_for_block_inclusion;
|
||||
use state_processing::per_block_processing::{
|
||||
compute_timestamp_at_slot, get_expected_withdrawals, verify_attestation_for_block_inclusion,
|
||||
};
|
||||
use state_processing::{
|
||||
BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures,
|
||||
};
|
||||
use state_processing::{VerifyOperation, state_advance::complete_state_advance};
|
||||
use tracing::{Span, debug, debug_span, error, instrument, trace, warn};
|
||||
use task_executor::JoinHandle;
|
||||
use tracing::{Instrument, Span, debug, debug_span, error, instrument, trace, warn};
|
||||
use tree_hash::TreeHash;
|
||||
use types::consts::gloas::{
|
||||
BID_VALUE_SELF_BUILD, BUILDER_INDEX_SELF_BUILD, EXECUTION_PAYMENT_TRUSTLESS_BUILD,
|
||||
};
|
||||
use types::{
|
||||
Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra,
|
||||
BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BlockProductionVersion,
|
||||
BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionPayloadBid, ExecutionPayloadEnvelope,
|
||||
ExecutionPayloadGloas, ExecutionRequests, FullPayload, Graffiti, Hash256, PayloadAttestation,
|
||||
ProposerSlashing, RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange,
|
||||
SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot,
|
||||
SyncAggregate,
|
||||
BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError,
|
||||
BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid,
|
||||
ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, FullPayload, Graffiti,
|
||||
Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock,
|
||||
SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope,
|
||||
SignedVoluntaryExit, Slot, SyncAggregate, Withdrawal, Withdrawals,
|
||||
};
|
||||
|
||||
use crate::execution_payload::get_execution_payload;
|
||||
use crate::{
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError,
|
||||
ProduceBlockVerification, graffiti_calculator::GraffitiSettings, metrics,
|
||||
@@ -38,6 +42,9 @@ use crate::{
|
||||
type ConsensusBlockValue = u64;
|
||||
type BlockProductionResult<E> = (BeaconBlock<E, FullPayload<E>>, ConsensusBlockValue);
|
||||
|
||||
pub type PreparePayloadResult<E> = Result<BlockProposalContentsGloas<E>, BlockProductionError>;
|
||||
pub type PreparePayloadHandle<E> = JoinHandle<Option<PreparePayloadResult<E>>>;
|
||||
|
||||
pub struct PartialBeaconBlock<E: EthSpec> {
|
||||
slot: Slot,
|
||||
proposer_index: u64,
|
||||
@@ -687,53 +694,26 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
// TODO(gloas) this should be BlockProductionVersion::V4
|
||||
// V3 is okay for now as long as we're not connected to a builder
|
||||
// TODO(gloas) add builder boost factor
|
||||
let prepare_payload_handle = get_execution_payload(
|
||||
let prepare_payload_handle = get_execution_payload_gloas(
|
||||
self.clone(),
|
||||
&state,
|
||||
parent_root,
|
||||
proposer_index,
|
||||
builder_params,
|
||||
None,
|
||||
BlockProductionVersion::V3,
|
||||
)?;
|
||||
|
||||
let block_contents_type = prepare_payload_handle
|
||||
let block_proposal_contents = prepare_payload_handle
|
||||
.await
|
||||
.map_err(BlockProductionError::TokioJoin)?
|
||||
.ok_or(BlockProductionError::ShuttingDown)??;
|
||||
|
||||
let (execution_payload, blob_kzg_commitments, execution_requests) =
|
||||
match block_contents_type {
|
||||
BlockProposalContentsType::Full(block_proposal_contents) => {
|
||||
let (payload, blob_kzg_commitments, _, execution_requests, _) =
|
||||
block_proposal_contents.deconstruct();
|
||||
|
||||
if let Some(blob_kzg_commitments) = blob_kzg_commitments
|
||||
&& let Some(execution_requests) = execution_requests
|
||||
{
|
||||
(
|
||||
payload.execution_payload(),
|
||||
blob_kzg_commitments,
|
||||
execution_requests,
|
||||
)
|
||||
} else {
|
||||
return Err(BlockProductionError::MissingKzgCommitment(
|
||||
"No KZG commitments from the payload".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
BlockProposalContentsType::Blinded(_) => {
|
||||
return Err(BlockProductionError::Unexpected(
|
||||
"Should never produce a blinded block post-Gloas".to_owned(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(gloas) this is just a dummy error variant for now
|
||||
let execution_payload_gloas = execution_payload
|
||||
.as_gloas()
|
||||
.map_err(|_| BlockProductionError::GloasNotImplemented)?
|
||||
.to_owned();
|
||||
let BlockProposalContentsGloas {
|
||||
payload,
|
||||
payload_value: _,
|
||||
execution_requests,
|
||||
blob_kzg_commitments,
|
||||
blobs_and_proofs: _,
|
||||
} = block_proposal_contents;
|
||||
|
||||
let state_root = state.update_tree_hash_cache()?;
|
||||
|
||||
@@ -742,10 +722,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let bid = ExecutionPayloadBid::<T::EthSpec> {
|
||||
parent_block_hash: state.latest_block_hash()?.to_owned(),
|
||||
parent_block_root: state.get_latest_block_root(state_root),
|
||||
block_hash: execution_payload.block_hash(),
|
||||
prev_randao: execution_payload.prev_randao(),
|
||||
block_hash: payload.block_hash,
|
||||
prev_randao: payload.prev_randao,
|
||||
fee_recipient: Address::ZERO,
|
||||
gas_limit: execution_payload.gas_limit(),
|
||||
gas_limit: payload.gas_limit,
|
||||
builder_index,
|
||||
slot: produce_at_slot,
|
||||
value: bid_value,
|
||||
@@ -755,7 +735,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
|
||||
// Store payload data for envelope construction after block is created
|
||||
let payload_data = ExecutionPayloadData {
|
||||
payload: execution_payload_gloas,
|
||||
payload,
|
||||
execution_requests,
|
||||
builder_index,
|
||||
slot: produce_at_slot,
|
||||
@@ -777,3 +757,146 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets an execution payload for inclusion in a block.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Will return an error when using a pre-Gloas `state`. Ensure to only run this function
|
||||
/// after the Gloas fork.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Equivalent to the `get_execution_payload` function in the Validator Guide:
|
||||
///
|
||||
/// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md#block-proposal
|
||||
fn get_execution_payload_gloas<T: BeaconChainTypes>(
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
state: &BeaconState<T::EthSpec>,
|
||||
parent_beacon_block_root: Hash256,
|
||||
proposer_index: u64,
|
||||
builder_params: BuilderParams,
|
||||
) -> Result<PreparePayloadHandle<T::EthSpec>, BlockProductionError> {
|
||||
// Compute all required values from the `state` now to avoid needing to pass it into a spawned
|
||||
// task.
|
||||
let spec = &chain.spec;
|
||||
let current_epoch = state.current_epoch();
|
||||
let timestamp =
|
||||
compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?;
|
||||
let random = *state.get_randao_mix(current_epoch)?;
|
||||
|
||||
let latest_execution_block_hash = *state.latest_block_hash()?;
|
||||
let latest_gas_limit = state.latest_execution_payload_bid()?.gas_limit;
|
||||
|
||||
let withdrawals =
|
||||
Withdrawals::<T::EthSpec>::from(get_expected_withdrawals(state, spec)?).into();
|
||||
|
||||
// Spawn a task to obtain the execution payload from the EL via a series of async calls. The
|
||||
// `join_handle` can be used to await the result of the function.
|
||||
let join_handle = chain
|
||||
.task_executor
|
||||
.clone()
|
||||
.spawn_handle(
|
||||
async move {
|
||||
prepare_execution_payload::<T>(
|
||||
&chain,
|
||||
timestamp,
|
||||
random,
|
||||
proposer_index,
|
||||
latest_execution_block_hash,
|
||||
latest_gas_limit,
|
||||
builder_params,
|
||||
withdrawals,
|
||||
parent_beacon_block_root,
|
||||
)
|
||||
.await
|
||||
}
|
||||
.instrument(debug_span!("prepare_execution_payload")),
|
||||
"prepare_execution_payload",
|
||||
)
|
||||
.ok_or(BlockProductionError::ShuttingDown)?;
|
||||
|
||||
Ok(join_handle)
|
||||
}
|
||||
|
||||
/// Prepares an execution payload for inclusion in a block.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Will return an error when using a pre-Gloas fork `state`. Ensure to only run this function
|
||||
/// after the Gloas fork.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Equivalent to the `prepare_execution_payload` function in the Validator Guide:
|
||||
///
|
||||
/// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md#block-proposal
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn prepare_execution_payload<T>(
|
||||
chain: &Arc<BeaconChain<T>>,
|
||||
timestamp: u64,
|
||||
random: Hash256,
|
||||
proposer_index: u64,
|
||||
parent_block_hash: ExecutionBlockHash,
|
||||
parent_gas_limit: u64,
|
||||
builder_params: BuilderParams,
|
||||
withdrawals: Vec<Withdrawal>,
|
||||
parent_beacon_block_root: Hash256,
|
||||
) -> Result<BlockProposalContentsGloas<T::EthSpec>, BlockProductionError>
|
||||
where
|
||||
T: BeaconChainTypes,
|
||||
{
|
||||
let spec = &chain.spec;
|
||||
let fork = spec.fork_name_at_slot::<T::EthSpec>(builder_params.slot);
|
||||
let execution_layer = chain
|
||||
.execution_layer
|
||||
.as_ref()
|
||||
.ok_or(BlockProductionError::ExecutionLayerMissing)?;
|
||||
|
||||
// Try to obtain the fork choice update parameters from the cached head.
|
||||
//
|
||||
// Use a blocking task to interact with the `canonical_head` lock otherwise we risk blocking the
|
||||
// core `tokio` executor.
|
||||
let inner_chain = chain.clone();
|
||||
let forkchoice_update_params = chain
|
||||
.spawn_blocking_handle(
|
||||
move || {
|
||||
inner_chain
|
||||
.canonical_head
|
||||
.cached_head()
|
||||
.forkchoice_update_parameters()
|
||||
},
|
||||
"prepare_execution_payload_forkchoice_update_params",
|
||||
)
|
||||
.instrument(debug_span!("forkchoice_update_params"))
|
||||
.await
|
||||
.map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?;
|
||||
|
||||
let suggested_fee_recipient = execution_layer
|
||||
.get_suggested_fee_recipient(proposer_index)
|
||||
.await;
|
||||
let payload_attributes = PayloadAttributes::new(
|
||||
timestamp,
|
||||
random,
|
||||
suggested_fee_recipient,
|
||||
Some(withdrawals),
|
||||
Some(parent_beacon_block_root),
|
||||
);
|
||||
|
||||
let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await;
|
||||
let payload_parameters = PayloadParameters {
|
||||
parent_hash: parent_block_hash,
|
||||
parent_gas_limit,
|
||||
proposer_gas_limit: target_gas_limit,
|
||||
payload_attributes: &payload_attributes,
|
||||
forkchoice_update_params: &forkchoice_update_params,
|
||||
current_fork: fork,
|
||||
};
|
||||
|
||||
let block_contents = execution_layer
|
||||
.get_payload_gloas(payload_parameters)
|
||||
.await
|
||||
.map_err(BlockProductionError::GetPayloadFailed)?;
|
||||
|
||||
Ok(block_contents)
|
||||
}
|
||||
|
||||
@@ -368,6 +368,13 @@ impl GossipTester {
|
||||
self.harness.chain.epoch().unwrap()
|
||||
}
|
||||
|
||||
pub fn is_gloas(&self) -> bool {
|
||||
self.harness
|
||||
.spec
|
||||
.fork_name_at_slot::<E>(self.valid_attestation.data.slot)
|
||||
.gloas_enabled()
|
||||
}
|
||||
|
||||
pub fn earliest_valid_attestation_slot(&self) -> Slot {
|
||||
let offset = if self
|
||||
.harness
|
||||
@@ -522,6 +529,44 @@ impl GossipTester {
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Like `inspect_aggregate_err`, but only runs the check if gloas is enabled.
|
||||
/// If gloas is not enabled, this is a no-op that returns self.
|
||||
pub fn inspect_aggregate_err_if_gloas<G, I>(
|
||||
self,
|
||||
desc: &str,
|
||||
get_attn: G,
|
||||
inspect_err: I,
|
||||
) -> Self
|
||||
where
|
||||
G: Fn(&Self, &mut SignedAggregateAndProof<E>),
|
||||
I: Fn(&Self, AttnError),
|
||||
{
|
||||
if self.is_gloas() {
|
||||
self.inspect_aggregate_err(desc, get_attn, inspect_err)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `inspect_unaggregate_err`, but only runs the check if gloas is enabled.
|
||||
/// If gloas is not enabled, this is a no-op that returns self.
|
||||
pub fn inspect_unaggregate_err_if_gloas<G, I>(
|
||||
self,
|
||||
desc: &str,
|
||||
get_attn: G,
|
||||
inspect_err: I,
|
||||
) -> Self
|
||||
where
|
||||
G: Fn(&Self, &mut SingleAttestation, &mut SubnetId, &ChainSpec),
|
||||
I: Fn(&Self, AttnError),
|
||||
{
|
||||
if self.is_gloas() {
|
||||
self.inspect_unaggregate_err(desc, get_attn, inspect_err)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Tests verification of `SignedAggregateAndProof` from the gossip network.
|
||||
#[tokio::test]
|
||||
@@ -854,6 +899,27 @@ async fn aggregated_gossip_verification() {
|
||||
))
|
||||
},
|
||||
)
|
||||
/*
|
||||
* [New in Gloas]: attestation.data.index must be < 2
|
||||
*/
|
||||
.inspect_aggregate_err_if_gloas(
|
||||
"gloas: aggregate with index >= 2",
|
||||
|_, a| match a.to_mut() {
|
||||
SignedAggregateAndProofRefMut::Base(_) => {
|
||||
panic!("Expected Electra attestation variant");
|
||||
}
|
||||
SignedAggregateAndProofRefMut::Electra(att) => {
|
||||
att.message.aggregate.data.index = 2;
|
||||
}
|
||||
},
|
||||
|_, err| {
|
||||
assert!(
|
||||
matches!(err, AttnError::CommitteeIndexInvalid),
|
||||
"expected CommitteeIndexInvalid, got {:?}",
|
||||
err
|
||||
)
|
||||
},
|
||||
)
|
||||
// NOTE: from here on, the tests are stateful, and rely on the valid attestation having
|
||||
// been seen.
|
||||
.import_valid_aggregate()
|
||||
@@ -1071,6 +1137,22 @@ async fn unaggregated_gossip_verification() {
|
||||
))
|
||||
},
|
||||
)
|
||||
/*
|
||||
* [New in Gloas]: attestation.data.index must be < 2
|
||||
*/
|
||||
.inspect_unaggregate_err_if_gloas(
|
||||
"gloas: attestation with index >= 2",
|
||||
|_, a, _, _| {
|
||||
a.data.index = 2;
|
||||
},
|
||||
|_, err| {
|
||||
assert!(
|
||||
matches!(err, AttnError::CommitteeIndexInvalid),
|
||||
"expected CommitteeIndexInvalid, got {:?}",
|
||||
err
|
||||
)
|
||||
},
|
||||
)
|
||||
// NOTE: from here on, the tests are stateful, and rely on the valid attestation having
|
||||
// been seen.
|
||||
.import_valid_unaggregate()
|
||||
@@ -1700,3 +1782,180 @@ async fn aggregated_attestation_verification_use_head_state_fork() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [New in Gloas]: Tests that unaggregated attestations with `data.index == 1` are rejected
|
||||
/// when `head_block.slot == attestation.data.slot`.
|
||||
///
|
||||
/// This test only runs when `FORK_NAME=gloas` is set with `fork_from_env` feature.
|
||||
// TODO(EIP-7732): Enable this test once gloas block production works in test harness.
|
||||
// `state.latest_execution_payload_header()` not available in Gloas.
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn gloas_unaggregated_attestation_same_slot_index_must_be_zero() {
|
||||
let harness = get_harness(VALIDATOR_COUNT);
|
||||
|
||||
// Skip this test if not running with gloas fork
|
||||
if !harness
|
||||
.spec
|
||||
.fork_name_at_epoch(Epoch::new(0))
|
||||
.gloas_enabled()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Extend the chain out a few epochs so we have some chain depth to play with.
|
||||
harness
|
||||
.extend_chain(
|
||||
MainnetEthSpec::slots_per_epoch() as usize * 3 - 1,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Produce a block in the current slot (this creates the same-slot scenario)
|
||||
harness
|
||||
.extend_chain(
|
||||
1,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::SomeValidators(vec![]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let current_slot = harness.chain.slot().expect("should get slot");
|
||||
let head = harness.chain.head_snapshot();
|
||||
|
||||
// Verify head block is in the current slot
|
||||
assert_eq!(
|
||||
head.beacon_block.slot(),
|
||||
current_slot,
|
||||
"head block should be in current slot for same-slot test"
|
||||
);
|
||||
|
||||
// Produce an attestation for the current slot
|
||||
let (mut attestation, _attester_sk, subnet_id) =
|
||||
get_valid_unaggregated_attestation(&harness.chain);
|
||||
|
||||
// Verify we have a same-slot scenario
|
||||
let attested_block_slot = harness
|
||||
.chain
|
||||
.canonical_head
|
||||
.fork_choice_read_lock()
|
||||
.get_block(&attestation.data.beacon_block_root)
|
||||
.expect("block should exist")
|
||||
.slot;
|
||||
assert_eq!(
|
||||
attested_block_slot, attestation.data.slot,
|
||||
"attested block slot should equal attestation slot for same-slot test"
|
||||
);
|
||||
|
||||
// index == 1 should be rejected when head_block.slot == attestation.data.slot
|
||||
attestation.data.index = 1;
|
||||
let result = harness
|
||||
.chain
|
||||
.verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id));
|
||||
assert!(
|
||||
matches!(result, Err(AttnError::CommitteeIndexNonZero(_))),
|
||||
"gloas: attestation with index == 1 when head_block.slot == attestation.data.slot should be rejected, got {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
/// [New in Gloas]: Tests that aggregated attestations with `data.index == 1` are rejected
|
||||
/// when `head_block.slot == attestation.data.slot`.
|
||||
///
|
||||
/// This test only runs when `FORK_NAME=gloas` is set with `fork_from_env` feature.
|
||||
// TODO(EIP-7732): Enable this test once gloas block production works in test harness.
|
||||
// `state.latest_execution_payload_header()` not available in Gloas.
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn gloas_aggregated_attestation_same_slot_index_must_be_zero() {
|
||||
let harness = get_harness(VALIDATOR_COUNT);
|
||||
|
||||
// Skip this test if not running with gloas fork
|
||||
if !harness
|
||||
.spec
|
||||
.fork_name_at_epoch(Epoch::new(0))
|
||||
.gloas_enabled()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Extend the chain out a few epochs so we have some chain depth to play with.
|
||||
harness
|
||||
.extend_chain(
|
||||
MainnetEthSpec::slots_per_epoch() as usize * 3 - 1,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Produce a block in the current slot (this creates the same-slot scenario)
|
||||
harness
|
||||
.extend_chain(
|
||||
1,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::SomeValidators(vec![]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let current_slot = harness.chain.slot().expect("should get slot");
|
||||
let head = harness.chain.head_snapshot();
|
||||
|
||||
// Verify head block is in the current slot
|
||||
assert_eq!(
|
||||
head.beacon_block.slot(),
|
||||
current_slot,
|
||||
"head block should be in current slot for same-slot test"
|
||||
);
|
||||
|
||||
// Produce an attestation for the current slot
|
||||
let (valid_attestation, _attester_sk, _subnet_id) =
|
||||
get_valid_unaggregated_attestation(&harness.chain);
|
||||
|
||||
// Verify we have a same-slot scenario
|
||||
let attested_block_slot = harness
|
||||
.chain
|
||||
.canonical_head
|
||||
.fork_choice_read_lock()
|
||||
.get_block(&valid_attestation.data.beacon_block_root)
|
||||
.expect("block should exist")
|
||||
.slot;
|
||||
assert_eq!(
|
||||
attested_block_slot, valid_attestation.data.slot,
|
||||
"attested block slot should equal attestation slot for same-slot test"
|
||||
);
|
||||
|
||||
// Convert to aggregate
|
||||
let committee = head
|
||||
.beacon_state
|
||||
.get_beacon_committee(current_slot, valid_attestation.committee_index)
|
||||
.expect("should get committee");
|
||||
let fork_name = harness
|
||||
.spec
|
||||
.fork_name_at_slot::<E>(valid_attestation.data.slot);
|
||||
let aggregate_attestation =
|
||||
single_attestation_to_attestation(&valid_attestation, committee.committee, fork_name)
|
||||
.unwrap();
|
||||
|
||||
let (mut valid_aggregate, _, _) =
|
||||
get_valid_aggregated_attestation(&harness.chain, aggregate_attestation);
|
||||
|
||||
// index == 1 should be rejected when head_block.slot == attestation.data.slot
|
||||
match valid_aggregate.to_mut() {
|
||||
SignedAggregateAndProofRefMut::Base(att) => {
|
||||
att.message.aggregate.data.index = 1;
|
||||
}
|
||||
SignedAggregateAndProofRefMut::Electra(att) => {
|
||||
att.message.aggregate.data.index = 1;
|
||||
}
|
||||
}
|
||||
|
||||
let result = harness
|
||||
.chain
|
||||
.verify_aggregated_attestation_for_gossip(&valid_aggregate);
|
||||
assert!(
|
||||
matches!(result, Err(AttnError::CommitteeIndexNonZero(_))),
|
||||
"gloas: aggregate with index == 1 when head_block.slot == attestation.data.slot should be rejected, got {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ use tree_hash::TreeHash;
|
||||
use types::builder::BuilderBid;
|
||||
use types::execution::BlockProductionVersion;
|
||||
use types::kzg_ext::KzgCommitments;
|
||||
use types::new_non_zero_usize;
|
||||
use types::{
|
||||
AbstractExecPayload, BlobsList, ExecutionPayloadDeneb, ExecutionRequests, KzgProofs,
|
||||
SignedBlindedBeaconBlock,
|
||||
@@ -58,6 +57,7 @@ use types::{
|
||||
ExecutionPayloadCapella, ExecutionPayloadElectra, ExecutionPayloadFulu, FullPayload,
|
||||
ProposerPreparationData, Slot,
|
||||
};
|
||||
use types::{ExecutionPayloadGloas, new_non_zero_usize};
|
||||
|
||||
mod block_hash;
|
||||
mod engine_api;
|
||||
@@ -168,6 +168,7 @@ pub enum Error {
|
||||
BeaconStateError(BeaconStateError),
|
||||
PayloadTypeMismatch,
|
||||
VerifyingVersionedHashes(versioned_hashes::Error),
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
impl From<ssz_types::Error> for Error {
|
||||
@@ -204,6 +205,26 @@ pub enum BlockProposalContentsType<E: EthSpec> {
|
||||
Blinded(BlockProposalContents<E, BlindedPayload<E>>),
|
||||
}
|
||||
|
||||
pub struct BlockProposalContentsGloas<E: EthSpec> {
|
||||
pub payload: ExecutionPayloadGloas<E>,
|
||||
pub payload_value: Uint256,
|
||||
pub blob_kzg_commitments: KzgCommitments<E>,
|
||||
pub blobs_and_proofs: (BlobsList<E>, KzgProofs<E>),
|
||||
pub execution_requests: ExecutionRequests<E>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> From<GetPayloadResponseGloas<E>> for BlockProposalContentsGloas<E> {
|
||||
fn from(response: GetPayloadResponseGloas<E>) -> Self {
|
||||
Self {
|
||||
payload: response.execution_payload,
|
||||
payload_value: response.block_value,
|
||||
blob_kzg_commitments: response.blobs_bundle.commitments,
|
||||
blobs_and_proofs: (response.blobs_bundle.blobs, response.blobs_bundle.proofs),
|
||||
execution_requests: response.requests,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum BlockProposalContents<E: EthSpec, Payload: AbstractExecPayload<E>> {
|
||||
Payload {
|
||||
payload: Payload,
|
||||
@@ -884,6 +905,43 @@ impl<E: EthSpec> ExecutionLayer<E> {
|
||||
.and_then(|entry| entry.gas_limit)
|
||||
}
|
||||
|
||||
/// Maps to the `engine_getPayload` JSON-RPC call for post-Gloas payload construction.
|
||||
///
|
||||
/// However, it will attempt to call `self.prepare_payload` if it cannot find an existing
|
||||
/// payload id for the given parameters.
|
||||
///
|
||||
/// ## Fallback Behavior
|
||||
///
|
||||
/// The result will be returned from the first node that returns successfully. No more nodes
|
||||
/// will be contacted.
|
||||
pub async fn get_payload_gloas(
|
||||
&self,
|
||||
payload_parameters: PayloadParameters<'_>,
|
||||
) -> Result<BlockProposalContentsGloas<E>, Error> {
|
||||
let payload_response_type = self.get_full_payload_caching(payload_parameters).await?;
|
||||
let GetPayloadResponseType::Full(payload_response) = payload_response_type else {
|
||||
return Err(Error::Unexpected(
|
||||
"get_payload_gloas should never return a blinded payload".to_owned(),
|
||||
));
|
||||
};
|
||||
let GetPayloadResponse::Gloas(payload_response) = payload_response else {
|
||||
return Err(Error::Unexpected(
|
||||
"get_payload_gloas should always return a gloas `GetPayloadResponse` variant"
|
||||
.to_owned(),
|
||||
));
|
||||
};
|
||||
metrics::inc_counter_vec(
|
||||
&metrics::EXECUTION_LAYER_GET_PAYLOAD_OUTCOME,
|
||||
&[metrics::SUCCESS],
|
||||
);
|
||||
metrics::inc_counter_vec(
|
||||
&metrics::EXECUTION_LAYER_GET_PAYLOAD_SOURCE,
|
||||
&[metrics::LOCAL],
|
||||
);
|
||||
|
||||
Ok(payload_response.into())
|
||||
}
|
||||
|
||||
/// Maps to the `engine_getPayload` JSON-RPC call.
|
||||
///
|
||||
/// However, it will attempt to call `self.prepare_payload` if it cannot find an existing
|
||||
|
||||
@@ -2422,6 +2422,25 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
"attn_comm_index_non_zero",
|
||||
);
|
||||
}
|
||||
AttnError::CommitteeIndexInvalid => {
|
||||
/*
|
||||
* The committee index is invalid after Gloas.
|
||||
*
|
||||
* The peer has published an invalid consensus message.
|
||||
*/
|
||||
debug!(
|
||||
%peer_id,
|
||||
block = ?beacon_block_root,
|
||||
?attestation_type,
|
||||
"Committee index invalid"
|
||||
);
|
||||
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject);
|
||||
self.gossip_penalize_peer(
|
||||
peer_id,
|
||||
PeerAction::LowToleranceError,
|
||||
"attn_comm_index_invalid",
|
||||
);
|
||||
}
|
||||
AttnError::UnknownHeadBlock { beacon_block_root } => {
|
||||
trace!(
|
||||
%peer_id,
|
||||
|
||||
@@ -2021,7 +2021,7 @@ impl<E: EthSpec> PublishBlockRequest<E> {
|
||||
|
||||
/// SSZ decode with fork variant determined by `fork_name`.
|
||||
pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result<Self, DecodeError> {
|
||||
if fork_name.deneb_enabled() {
|
||||
if fork_name.deneb_enabled() && !fork_name.gloas_enabled() {
|
||||
let mut builder = ssz::SszDecoderBuilder::new(bytes);
|
||||
builder.register_anonymous_variable_length_item()?;
|
||||
builder.register_type::<KzgProofs<E>>()?;
|
||||
|
||||
Reference in New Issue
Block a user