Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 68 additions & 16 deletions beacon_node/beacon_chain/src/block_production/gloas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use execution_layer::{
BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters,
};
use operation_pool::CompactAttestationRef;
use ssz::{BitVector, Encode, ProgressiveBitList, TryFromIter};
use ssz::{Encode, ProgressiveBitList, TryFromIter};
use ssz_types::ProgressiveVariableList;
use state_processing::common::{get_attesting_indices_from_state, get_indexed_payload_attestation};
use state_processing::envelope_processing::verify_execution_payload_envelope;
Expand All @@ -32,8 +32,8 @@ use types::{
BeaconBlockBodyGloas, BeaconBlockBodyHeze, BeaconBlockGloas, BeaconBlockHeze, BeaconState,
BeaconStateError, BlobsList, BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionBlockHash,
ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadEnvelope,
ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, IndexedAttestation, KzgProofs,
PayloadAttestation, ProgressiveTransactions, ProposerSlashing, RelativeEpoch,
ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, InclusionListBits, IndexedAttestation,
KzgProofs, PayloadAttestation, ProgressiveTransactions, ProposerSlashing, RelativeEpoch,
SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid,
SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope,
SignedExecutionPayloadEnvelopeSummary, SignedProposerPreferences, SignedVoluntaryExit, Slot,
Expand All @@ -48,7 +48,7 @@ use crate::block_production::bid_selection::{self, BidCandidate, BidSource, Exec
use crate::payload_bid_verification::PayloadBidError;
use crate::payload_bid_verification::direct_verified_bid::verify_direct_bid;
use crate::payload_bid_verification::gossip_verified_bid::{
builder_exit_requested, verify_bid_state_conditions,
builder_exit_requested, verify_bid_inclusion_list_bits, verify_bid_state_conditions,
};
use crate::payload_bid_verification::payload_bid_cache::BidParent;
use crate::pending_payload_envelopes::PendingEnvelopeData;
Expand Down Expand Up @@ -282,6 +282,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.gossip_verified_proposer_preferences_cache
.get_preferences(&produce_at_slot, dependent_root);

// The node's own inclusion list bits for the slot before the proposal.
// This contains every stored list, both timely and untimely.
// The self-bid claims them and every external bid must cover them.
let local_inclusion_list_bits = self.local_inclusion_list_bits(&ctx).await;

// Fire the direct builder fan-out concurrently with the local EL payload build: both only
// read `state`, so they race without contention. A local EL failure is not fatal — we fall
// back to an external bid when one is available; only a total absence of viable bids fails
Expand All @@ -290,6 +295,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
ctx,
&builder_config,
proposer_preferences.as_deref(),
&local_inclusion_list_bits,
&state,
&parent_execution_requests,
);
Expand All @@ -301,6 +307,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
BUILDER_INDEX_SELF_BUILD,
executed_ancestor_hash,
proposer_preferences.as_deref(),
&local_inclusion_list_bits,
);
let (mut candidates, local_result) = tokio::join!(acquire_fut, local_fut);

Expand Down Expand Up @@ -961,6 +968,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
builder_index: BuilderIndex,
executed_ancestor_hash: ExecutionBlockHash,
proposer_preferences: Option<&SignedProposerPreferences>,
inclusion_list_bits: &InclusionListBits<T::EthSpec>,
) -> Result<
(
SignedExecutionPayloadBid<T::EthSpec>,
Expand Down Expand Up @@ -1075,9 +1083,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD,
blob_kzg_commitments,
execution_requests_root: execution_requests.tree_hash_root(),
// TODO(heze): set the bits based on the IL committee members whose
// inclusion lists were considered when building the payload
inclusion_list_bits: BitVector::new(),
inclusion_list_bits: inclusion_list_bits.clone(),
};
SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze {
message: bid,
Expand Down Expand Up @@ -1115,14 +1121,17 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
///
/// Direct bids are requested only when there are configured builders to contact and the proposer
/// submitted preferences to validate against (`proposer_preferences`, needed for a direct bid's
/// gas limit and fee recipient). Acquisition is best-effort: any direct failure — including a
/// missing builder service — is logged and skipped, never aborting block production, which can
/// still proceed on the local build and gossip bids.
/// gas limit and fee recipient). From Heze, every external bid's `inclusion_list_bits` must
/// also cover `local_inclusion_list_bits`, the node's own view of the previous slot's inclusion
/// lists. Acquisition is best-effort: any direct failure — including a missing builder service
/// — is logged and skipped, never aborting block production, which can still proceed on the
/// local build and gossip bids.
async fn acquire_external_bid_candidates(
self: &Arc<Self>,
ctx: BidRequestContext,
builder_config: &BuilderConfig,
proposer_preferences: Option<&SignedProposerPreferences>,
local_inclusion_list_bits: &InclusionListBits<T::EthSpec>,
state: &BeaconState<T::EthSpec>,
parent_execution_requests: &ExecutionRequestsGloas<T::EthSpec>,
) -> Vec<BidCandidate<T::EthSpec>> {
Expand All @@ -1137,6 +1146,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
&ctx,
builder_config,
proposer_preferences,
local_inclusion_list_bits,
state,
)
.await,
Expand All @@ -1158,11 +1168,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
parent_block_root: ctx.parent_root,
},
) {
// The gossip bid was validated against the head state at gossip time; its builder's
// eligibility or coverage can go stale before production. Re-check against the production
// state and drop it if it would now fail `per_block_processing`, so a stale gossip bid
// can't outrank a viable candidate and sink the whole proposal.
match verify_bid_state_conditions(gossip_bid.message(), state, &self.spec) {
// The gossip bid was validated against the head state at gossip time, with its
// inclusion list bits checked only against the timely lists; its builder's eligibility
// or coverage can go stale before production, and more inclusion lists can arrive.
// Re-check against the production state, drop the bid if it would now fail
// `per_block_processing`, and require its bits to cover every inclusion list the node
// holds now, so a stale gossip bid can't outrank a viable candidate and sink the whole
// proposal.
let result = verify_bid_state_conditions(gossip_bid.message(), state, &self.spec)
.and_then(|_| {
verify_bid_inclusion_list_bits(gossip_bid.message(), local_inclusion_list_bits)
});

match result {
Ok(_) => {
externals.push(BidCandidate::gossip(
gossip_bid,
Expand All @@ -1173,7 +1191,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
Err(error) => {
warn!(
?error,
"Skipping gossip bid that no longer passes state validation"
"Skipping gossip bid that fails validation at production"
);
}
}
Expand Down Expand Up @@ -1211,6 +1229,36 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
externals
}

/// The node's own inclusion list bits for the `ctx.slot - 1` slot. Every inclusion list is
/// considered, both timely and untimely. The bits are empty before Heze, and empty when the
/// view cannot be read.
async fn local_inclusion_list_bits(
self: &Arc<Self>,
ctx: &BidRequestContext,
) -> InclusionListBits<T::EthSpec> {
if !ctx.fork_name.heze_enabled() {
return Default::default();
}

let chain = self.clone();
let parent_root = ctx.parent_root;
let inclusion_list_slot = ctx.slot.saturating_sub(1u64);
let result = self
.spawn_blocking_handle(
move || chain.get_inclusion_list_bits(parent_root, inclusion_list_slot, false),
"local_inclusion_list_bits",
)
.await;

match result.and_then(|bits| bits) {
Ok(bits) => bits,
Err(error) => {
warn!(?error, "Unable to read inclusion list bits");
Default::default()
}
}
}

/// Request direct bids from the configured builders and return each valid one as a selection
/// candidate.
///
Expand All @@ -1224,6 +1272,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
ctx: &BidRequestContext,
builder_config: &BuilderConfig,
proposer_preferences: &SignedProposerPreferences,
local_inclusion_list_bits: &InclusionListBits<T::EthSpec>,
state: &BeaconState<T::EthSpec>,
) -> Vec<BidCandidate<T::EthSpec>> {
let Some(builders) = self.builders.as_ref() else {
Expand All @@ -1246,6 +1295,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let state = Arc::new(state.clone());
let spec = self.spec.clone();
let proposer_preferences = Arc::new(proposer_preferences.clone());
let local_inclusion_list_bits = local_inclusion_list_bits.clone();
let executor = self.task_executor.clone();
// The gas limit a direct bid must adjust from is that of the execution payload at the
// selected parent, which is the right baseline under either a FULL or EMPTY parent view.
Expand All @@ -1264,6 +1314,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let state = state.clone();
let spec = spec.clone();
let proposer_preferences = proposer_preferences.clone();
let local_inclusion_list_bits = local_inclusion_list_bits.clone();
let executor = executor.clone();
async move {
// The bid's BLS signature check is CPU-bound; run the whole verification on a
Expand All @@ -1290,6 +1341,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
executed_ancestor_gas_limit,
&expected_builder_pubkeys,
&proposer_preferences,
&local_inclusion_list_bits,
&state,
&spec,
)
Expand Down
33 changes: 22 additions & 11 deletions beacon_node/beacon_chain/src/inclusion_list_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
//! keyed by `(slot, dependent_root)`, which pins an inclusion list to the committee it was produced
//! against.

use ssz_types::{BitVector, ProgressiveVariableList};
use ssz_types::ProgressiveVariableList;
use std::collections::{HashMap, HashSet};
use std::marker::PhantomData;
use tree_hash::TreeHash;
use types::{ChainSpec, EthSpec, Hash256, InclusionListCommittee, SignedInclusionList, Slot};
use types::{
ChainSpec, EthSpec, Hash256, InclusionListBits, InclusionListCommittee, SignedInclusionList,
Slot,
};

/// The shuffling `dependent_root` an inclusion list was produced against.
pub type DependentRoot = Hash256;
Expand Down Expand Up @@ -63,6 +66,19 @@ pub struct InclusionListStore<E: EthSpec> {
_phantom: PhantomData<E>,
}

/// Returns whether `bits` is inclusive of `local`: every bit set in `local` is also set in `bits`.
pub fn inclusion_list_bits_are_inclusive<E: EthSpec>(
local: &InclusionListBits<E>,
bits: &InclusionListBits<E>,
) -> Result<bool, Error> {
for i in 0..local.len() {
if local.get(i)? && !bits.get(i)? {
return Ok(false);
}
}
Ok(true)
}

impl<E: EthSpec> InclusionListStore<E> {
pub fn new(spec: &ChainSpec) -> Self {
// `heze_fork_epoch` holds the far future sentinel when Heze is unscheduled.
Expand Down Expand Up @@ -211,10 +227,10 @@ impl<E: EthSpec> InclusionListStore<E> {
dependent_root: DependentRoot,
il_committee: &InclusionListCommittee<E>,
only_timely: bool,
) -> Result<BitVector<E::InclusionListCommitteeSize>, Error> {
) -> Result<InclusionListBits<E>, Error> {
let submitted = self.submitted_validators(slot, dependent_root, only_timely);

let mut bits = BitVector::new();
let mut bits = InclusionListBits::<E>::new();
for (i, validator) in il_committee.iter().enumerate() {
if submitted.contains(validator) {
bits.set(i, true)?;
Expand All @@ -230,17 +246,12 @@ impl<E: EthSpec> InclusionListStore<E> {
slot: Slot,
dependent_root: DependentRoot,
il_committee: &InclusionListCommittee<E>,
bits: &BitVector<E::InclusionListCommitteeSize>,
bits: &InclusionListBits<E>,
only_timely: bool,
) -> Result<bool, Error> {
let local =
self.get_inclusion_list_bits(slot, dependent_root, il_committee, only_timely)?;
for i in 0..local.len() {
if local.get(i)? && !bits.get(i)? {
return Ok(false);
}
}
Ok(true)
inclusion_list_bits_are_inclusive::<E>(&local, bits)
}

/// The stored signed inclusion lists for the given `validators`, used to serve
Expand Down
Loading
Loading