Move block production to gloas file (no logic change).

This commit is contained in:
Jimmy Chen
2026-02-05 10:53:13 +11:00
parent 75bb4288ff
commit f9bfaf9da6
6 changed files with 183 additions and 217 deletions

View File

@@ -0,0 +1,749 @@
use std::collections::HashMap;
use std::marker::PhantomData;
use std::sync::Arc;
use bls::Signature;
use execution_layer::{BlockProposalContentsType, BuilderParams};
use operation_pool::CompactAttestationRef;
use ssz::Encode;
use state_processing::common::get_attesting_indices_from_state;
use state_processing::epoch_cache::initialize_epoch_cache;
use state_processing::per_block_processing::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 tree_hash::TreeHash;
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, SignedVoluntaryExit, Slot, SyncAggregate,
};
use crate::execution_payload::get_execution_payload;
use crate::{
BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError,
ProduceBlockVerification, graffiti_calculator::GraffitiSettings, metrics,
};
pub struct PartialBeaconBlock<E: EthSpec> {
slot: Slot,
proposer_index: u64,
parent_root: Hash256,
randao_reveal: Signature,
eth1_data: Eth1Data,
graffiti: Graffiti,
proposer_slashings: Vec<ProposerSlashing>,
attester_slashings: Vec<AttesterSlashingElectra<E>>,
attestations: Vec<AttestationElectra<E>>,
payload_attestations: Vec<PayloadAttestation<E>>,
deposits: Vec<Deposit>,
voluntary_exits: Vec<SignedVoluntaryExit>,
sync_aggregate: Option<SyncAggregate<E>>,
bls_to_execution_changes: Vec<SignedBlsToExecutionChange>,
}
/// Data needed to construct an ExecutionPayloadEnvelope.
/// The envelope requires the beacon_block_root which can only be computed after the block exists.
pub struct ExecutionPayloadData<E: types::EthSpec> {
pub payload: ExecutionPayloadGloas<E>,
pub execution_requests: ExecutionRequests<E>,
pub builder_index: BuilderIndex,
pub slot: Slot,
pub state_root: Hash256,
}
impl<T: BeaconChainTypes> BeaconChain<T> {
pub async fn produce_block_with_verification_gloas(
self: &Arc<Self>,
randao_reveal: Signature,
slot: Slot,
graffiti_settings: GraffitiSettings,
verification: ProduceBlockVerification,
_builder_boost_factor: Option<u64>,
) -> Result<(BeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>, u64), BlockProductionError> {
metrics::inc_counter(&metrics::BLOCK_PRODUCTION_REQUESTS);
let _complete_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_TIMES);
// Part 1/2 (blocking)
//
// Load the parent state from disk.
let chain = self.clone();
let span = Span::current();
let (state, state_root_opt) = self
.task_executor
.spawn_blocking_handle(
move || {
let _guard =
debug_span!(parent: span, "load_state_for_block_production").entered();
chain.load_state_for_block_production(slot)
},
"load_state_for_block_production",
)
.ok_or(BlockProductionError::ShuttingDown)?
.await
.map_err(BlockProductionError::TokioJoin)??;
// Part 2/2 (async, with some blocking components)
//
// Produce the block upon the state
self.produce_block_on_state_gloas(
state,
state_root_opt,
slot,
randao_reveal,
graffiti_settings,
verification,
)
.await
}
// TODO(gloas) need to implement builder boost factor logic
pub async fn produce_block_on_state_gloas(
self: &Arc<Self>,
state: BeaconState<T::EthSpec>,
state_root_opt: Option<Hash256>,
produce_at_slot: Slot,
randao_reveal: Signature,
graffiti_settings: GraffitiSettings,
verification: ProduceBlockVerification,
) -> Result<(BeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>, u64), BlockProductionError> {
// Part 1/3 (blocking)
//
// Perform the state advance and block-packing functions.
let chain = self.clone();
let graffiti = self
.graffiti_calculator
.get_graffiti(graffiti_settings)
.await;
let span = Span::current();
let (partial_beacon_block, state) = self
.task_executor
.spawn_blocking_handle(
move || {
let _guard =
debug_span!(parent: span, "produce_partial_beacon_block_gloas").entered();
chain.produce_partial_beacon_block_gloas(
state,
state_root_opt,
produce_at_slot,
randao_reveal,
graffiti,
)
},
"produce_partial_beacon_block_gloas",
)
.ok_or(BlockProductionError::ShuttingDown)?
.await
.map_err(BlockProductionError::TokioJoin)??;
// Part 2/3 (async)
//
// Produce the execution payload bid.
// TODO(gloas) this is strictly for building local bids
// We'll need to build out trustless/trusted bid paths.
let (execution_payload_bid, state, payload_data) = self
.clone()
.produce_execution_payload_bid(state, state_root_opt, produce_at_slot, 0, u64::MAX)
.await?;
// Part 3/3 (blocking)
//
// Complete the block with the execution payload bid.
let chain = self.clone();
let span = Span::current();
self.task_executor
.spawn_blocking_handle(
move || {
let _guard =
debug_span!(parent: span, "complete_partial_beacon_block_gloas").entered();
chain.complete_partial_beacon_block_gloas(
partial_beacon_block,
execution_payload_bid,
payload_data,
state,
verification,
)
},
"complete_partial_beacon_block_gloas",
)
.ok_or(BlockProductionError::ShuttingDown)?
.await
.map_err(BlockProductionError::TokioJoin)?
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn produce_partial_beacon_block_gloas(
self: &Arc<Self>,
mut state: BeaconState<T::EthSpec>,
state_root_opt: Option<Hash256>,
produce_at_slot: Slot,
randao_reveal: Signature,
graffiti: Graffiti,
) -> Result<(PartialBeaconBlock<T::EthSpec>, BeaconState<T::EthSpec>), BlockProductionError>
{
// It is invalid to try to produce a block using a state from a future slot.
if state.slot() > produce_at_slot {
return Err(BlockProductionError::StateSlotTooHigh {
produce_at_slot,
state_slot: state.slot(),
});
}
let slot_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_SLOT_PROCESS_TIMES);
// Ensure the state has performed a complete transition into the required slot.
complete_state_advance(&mut state, state_root_opt, produce_at_slot, &self.spec)?;
drop(slot_timer);
state.build_committee_cache(RelativeEpoch::Current, &self.spec)?;
state.apply_pending_mutations()?;
let parent_root = if state.slot() > 0 {
*state
.get_block_root(state.slot() - 1)
.map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)?
} else {
state.latest_block_header().canonical_root()
};
let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64;
let slashings_and_exits_span = debug_span!("get_slashings_and_exits").entered();
let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) =
self.op_pool.get_slashings_and_exits(&state, &self.spec);
drop(slashings_and_exits_span);
let eth1_data = state.eth1_data().clone();
let deposits = vec![];
let bls_changes_span = debug_span!("get_bls_to_execution_changes").entered();
let bls_to_execution_changes = self
.op_pool
.get_bls_to_execution_changes(&state, &self.spec);
drop(bls_changes_span);
// Iterate through the naive aggregation pool and ensure all the attestations from there
// are included in the operation pool.
{
let _guard = debug_span!("import_naive_aggregation_pool").entered();
let _unagg_import_timer =
metrics::start_timer(&metrics::BLOCK_PRODUCTION_UNAGGREGATED_TIMES);
for attestation in self.naive_aggregation_pool.read().iter() {
let import = |attestation: &Attestation<T::EthSpec>| {
let attesting_indices =
get_attesting_indices_from_state(&state, attestation.to_ref())?;
self.op_pool
.insert_attestation(attestation.clone(), attesting_indices)
};
if let Err(e) = import(attestation) {
// Don't stop block production if there's an error, just create a log.
error!(
reason = ?e,
"Attestation did not transfer to op pool"
);
}
}
};
let mut attestations = {
let _guard = debug_span!("pack_attestations").entered();
let _attestation_packing_timer =
metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES);
// Epoch cache and total balance cache are required for op pool packing.
state.build_total_active_balance_cache(&self.spec)?;
initialize_epoch_cache(&mut state, &self.spec)?;
let mut prev_filter_cache = HashMap::new();
let prev_attestation_filter = |att: &CompactAttestationRef<T::EthSpec>| {
self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state)
};
let mut curr_filter_cache = HashMap::new();
let curr_attestation_filter = |att: &CompactAttestationRef<T::EthSpec>| {
self.filter_op_pool_attestation(&mut curr_filter_cache, att, &state)
};
self.op_pool
.get_attestations(
&state,
prev_attestation_filter,
curr_attestation_filter,
&self.spec,
)
.map_err(BlockProductionError::OpPoolError)?
};
// If paranoid mode is enabled re-check the signatures of every included message.
// This will be a lot slower but guards against bugs in block production and can be
// quickly rolled out without a release.
if self.config.paranoid_block_proposal {
let mut tmp_ctxt = ConsensusContext::new(state.slot());
attestations.retain(|att| {
verify_attestation_for_block_inclusion(
&state,
att.to_ref(),
&mut tmp_ctxt,
VerifySignatures::True,
&self.spec,
)
.map_err(|e| {
warn!(
err = ?e,
block_slot = %state.slot(),
attestation = ?att,
"Attempted to include an invalid attestation"
);
})
.is_ok()
});
proposer_slashings.retain(|slashing| {
slashing
.clone()
.validate(&state, &self.spec)
.map_err(|e| {
warn!(
err = ?e,
block_slot = %state.slot(),
?slashing,
"Attempted to include an invalid proposer slashing"
);
})
.is_ok()
});
attester_slashings.retain(|slashing| {
slashing
.clone()
.validate(&state, &self.spec)
.map_err(|e| {
warn!(
err = ?e,
block_slot = %state.slot(),
?slashing,
"Attempted to include an invalid attester slashing"
);
})
.is_ok()
});
voluntary_exits.retain(|exit| {
exit.clone()
.validate(&state, &self.spec)
.map_err(|e| {
warn!(
err = ?e,
block_slot = %state.slot(),
?exit,
"Attempted to include an invalid proposer slashing"
);
})
.is_ok()
});
// TODO(gloas) verifiy payload attestation signature here as well
}
let attester_slashings = attester_slashings
.into_iter()
.filter_map(|a| match a {
AttesterSlashing::Base(_) => None,
AttesterSlashing::Electra(a) => Some(a),
})
.collect::<Vec<_>>();
let attestations = attestations
.into_iter()
.filter_map(|a| match a {
Attestation::Base(_) => None,
Attestation::Electra(a) => Some(a),
})
.collect::<Vec<_>>();
let slot = state.slot();
let sync_aggregate = if matches!(&state, BeaconState::Base(_)) {
None
} else {
let sync_aggregate = self
.op_pool
.get_sync_aggregate(&state)
.map_err(BlockProductionError::OpPoolError)?
.unwrap_or_else(|| {
warn!(
slot = %state.slot(),
"Producing block with no sync contributions"
);
SyncAggregate::new()
});
Some(sync_aggregate)
};
Ok((
PartialBeaconBlock {
slot,
proposer_index,
parent_root,
randao_reveal,
eth1_data,
graffiti,
proposer_slashings,
attester_slashings,
attestations,
deposits,
voluntary_exits,
sync_aggregate,
// TODO(gloas) need to implement payload attestations
payload_attestations: vec![],
bls_to_execution_changes,
},
state,
))
}
#[allow(clippy::type_complexity)]
fn complete_partial_beacon_block_gloas(
&self,
partial_beacon_block: PartialBeaconBlock<T::EthSpec>,
signed_execution_payload_bid: SignedExecutionPayloadBid<T::EthSpec>,
payload_data: Option<ExecutionPayloadData<T::EthSpec>>,
mut state: BeaconState<T::EthSpec>,
verification: ProduceBlockVerification,
) -> Result<(BeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>, u64), BlockProductionError> {
let PartialBeaconBlock {
slot,
proposer_index,
parent_root,
randao_reveal,
eth1_data,
graffiti,
proposer_slashings,
attester_slashings,
attestations,
deposits,
voluntary_exits,
sync_aggregate,
payload_attestations,
bls_to_execution_changes,
} = partial_beacon_block;
let beacon_block = match &state {
BeaconState::Base(_) => {
return Err(BlockProductionError::InvalidBlockVariant(
"Cannot construct a block pre-Gloas".to_owned(),
));
}
BeaconState::Altair(_) => {
return Err(BlockProductionError::InvalidBlockVariant(
"Cannot construct a block pre-Gloas".to_owned(),
));
}
BeaconState::Bellatrix(_) => {
return Err(BlockProductionError::InvalidBlockVariant(
"Cannot construct a block pre-Gloas".to_owned(),
));
}
BeaconState::Capella(_) => {
return Err(BlockProductionError::InvalidBlockVariant(
"Cannot construct a block pre-Gloas".to_owned(),
));
}
BeaconState::Deneb(_) => {
return Err(BlockProductionError::InvalidBlockVariant(
"Cannot construct a block pre-Gloas".to_owned(),
));
}
BeaconState::Electra(_) => {
return Err(BlockProductionError::InvalidBlockVariant(
"Cannot construct a block pre-Gloas".to_owned(),
));
}
BeaconState::Fulu(_) => {
return Err(BlockProductionError::InvalidBlockVariant(
"Cannot construct a block pre-Gloas".to_owned(),
));
}
BeaconState::Gloas(_) => BeaconBlock::Gloas(BeaconBlockGloas {
slot,
proposer_index,
parent_root,
state_root: Hash256::ZERO,
body: BeaconBlockBodyGloas {
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: sync_aggregate
.ok_or(BlockProductionError::MissingSyncAggregate)?,
bls_to_execution_changes: bls_to_execution_changes
.try_into()
.map_err(BlockProductionError::SszTypesError)?,
signed_execution_payload_bid,
payload_attestations: payload_attestations
.try_into()
.map_err(BlockProductionError::SszTypesError)?,
_phantom: PhantomData::<FullPayload<T::EthSpec>>,
},
}),
};
let signed_beacon_block = SignedBeaconBlock::from_block(
beacon_block,
// The block is not signed here, that is the task of a validator client.
Signature::empty(),
);
let block_size = signed_beacon_block.ssz_bytes_len();
debug!(%block_size, "Produced block on state");
metrics::observe(&metrics::BLOCK_SIZE, block_size as f64);
if block_size > self.config.max_network_size {
return Err(BlockProductionError::BlockTooLarge(block_size));
}
let process_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_PROCESS_TIMES);
let signature_strategy = match verification {
ProduceBlockVerification::VerifyRandao => BlockSignatureStrategy::VerifyRandao,
ProduceBlockVerification::NoVerification => BlockSignatureStrategy::NoVerification,
};
// Use a context without block root or proposer index so that both are checked.
let mut ctxt = ConsensusContext::new(signed_beacon_block.slot());
let consensus_block_value = self
.compute_beacon_block_reward(signed_beacon_block.message(), &mut state)
.map(|reward| reward.total)
.unwrap_or(0);
state_processing::per_block_processing(
&mut state,
&signed_beacon_block,
signature_strategy,
VerifyBlockRoot::True,
&mut ctxt,
&self.spec,
)?;
drop(process_timer);
let state_root_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_ROOT_TIMES);
let state_root = state.update_tree_hash_cache()?;
drop(state_root_timer);
let (mut block, _) = signed_beacon_block.deconstruct();
*block.state_root_mut() = state_root;
// Construct and cache the ExecutionPayloadEnvelope if we have payload data.
// For local building, we always have payload data.
// For trustless building, the builder will provide the envelope separately.
if let Some(payload_data) = payload_data {
let beacon_block_root = block.tree_hash_root();
let execution_payload_envelope = ExecutionPayloadEnvelope {
payload: payload_data.payload,
execution_requests: payload_data.execution_requests,
builder_index: payload_data.builder_index,
beacon_block_root,
slot: payload_data.slot,
state_root: payload_data.state_root,
};
// Cache the envelope for later retrieval by the validator for signing and publishing.
let envelope_slot = payload_data.slot;
self.pending_payload_envelopes
.write()
.insert(envelope_slot, execution_payload_envelope);
debug!(
%beacon_block_root,
slot = %envelope_slot,
"Cached pending execution payload envelope"
);
}
metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES);
trace!(
parent = ?block.parent_root(),
attestations = block.body().attestations_len(),
slot = %block.slot(),
"Produced beacon block"
);
Ok((block, consensus_block_value))
}
// TODO(gloas) introduce `ProposerPreferences` so we can build out trustless
// bid building. Right now this only works for local building.
/// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`.
/// This function assumes we've already done the state advance.
///
/// Returns the signed bid, the state, and optionally the payload data needed to construct
/// the `ExecutionPayloadEnvelope` after the beacon block is created.
///
/// For local building, payload data is always returned (`Some`).
/// For trustless building, the builder provides the envelope separately, so `None` is returned.
#[allow(clippy::type_complexity)]
#[instrument(level = "debug", skip_all)]
pub async fn produce_execution_payload_bid(
self: Arc<Self>,
state: BeaconState<T::EthSpec>,
state_root_opt: Option<Hash256>,
produce_at_slot: Slot,
bid_value: u64,
builder_index: BuilderIndex,
) -> Result<
(
SignedExecutionPayloadBid<T::EthSpec>,
BeaconState<T::EthSpec>,
Option<ExecutionPayloadData<T::EthSpec>>,
),
BlockProductionError,
> {
// TODO(gloas) For non local building, add sanity check on value
// The builder MUST have enough excess balance to fulfill this bid (i.e. `value`) and all pending payments.
// TODO(gloas) add metrics for execution payload bid production
let parent_root = if state.slot() > 0 {
*state
.get_block_root(state.slot() - 1)
.map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)?
} else {
state.latest_block_header().canonical_root()
};
let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64;
let pubkey = state
.validators()
.get(proposer_index as usize)
.map(|v| v.pubkey)
.ok_or(BlockProductionError::BeaconChain(Box::new(
BeaconChainError::ValidatorIndexUnknown(proposer_index as usize),
)))?;
let builder_params = BuilderParams {
pubkey,
slot: state.slot(),
chain_health: self
.is_healthy(&parent_root)
.map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?,
};
// 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(
self.clone(),
&state,
parent_root,
proposer_index,
builder_params,
None,
BlockProductionVersion::V3,
)?;
let block_contents_type = 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(),
));
}
}
// TODO(gloas) we should never receive a blinded response.
// Should return some type of `Unexpected` error variant as this should never happen
// in the V4 block production flow
BlockProposalContentsType::Blinded(_) => {
return Err(BlockProductionError::GloasNotImplemented);
}
};
let state_root = state_root_opt.ok_or_else(|| BlockProductionError::MissingStateRoot)?;
// 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 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(),
fee_recipient: Address::ZERO,
gas_limit: execution_payload.gas_limit(),
builder_index,
slot: produce_at_slot,
value: bid_value,
execution_payment: 0,
blob_kzg_commitments,
};
// Store payload data for envelope construction after block is created
let payload_data = ExecutionPayloadData {
payload: execution_payload_gloas,
execution_requests,
builder_index,
slot: produce_at_slot,
state_root,
};
// TODO(gloas) this is only local building
// we'll need to implement builder signature for the trustless path
Ok((
SignedExecutionPayloadBid {
message: bid,
// TODO(gloas) return better error variant here
signature: Signature::infinity()
.map_err(|_| BlockProductionError::GloasNotImplemented)?,
},
state,
// Local building always returns payload data.
// Trustless building would return None here.
Some(payload_data),
))
}
}

View File

@@ -0,0 +1 @@
mod gloas;