diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 1a74e735893..7e054ddaac7 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -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; @@ -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, @@ -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; @@ -282,6 +282,11 @@ impl BeaconChain { .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 @@ -290,6 +295,7 @@ impl BeaconChain { ctx, &builder_config, proposer_preferences.as_deref(), + &local_inclusion_list_bits, &state, &parent_execution_requests, ); @@ -301,6 +307,7 @@ impl BeaconChain { 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); @@ -961,6 +968,7 @@ impl BeaconChain { builder_index: BuilderIndex, executed_ancestor_hash: ExecutionBlockHash, proposer_preferences: Option<&SignedProposerPreferences>, + inclusion_list_bits: &InclusionListBits, ) -> Result< ( SignedExecutionPayloadBid, @@ -1075,9 +1083,7 @@ impl BeaconChain { 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, @@ -1115,14 +1121,17 @@ impl BeaconChain { /// /// 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, ctx: BidRequestContext, builder_config: &BuilderConfig, proposer_preferences: Option<&SignedProposerPreferences>, + local_inclusion_list_bits: &InclusionListBits, state: &BeaconState, parent_execution_requests: &ExecutionRequestsGloas, ) -> Vec> { @@ -1137,6 +1146,7 @@ impl BeaconChain { &ctx, builder_config, proposer_preferences, + local_inclusion_list_bits, state, ) .await, @@ -1158,11 +1168,19 @@ impl BeaconChain { 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, @@ -1173,7 +1191,7 @@ impl BeaconChain { Err(error) => { warn!( ?error, - "Skipping gossip bid that no longer passes state validation" + "Skipping gossip bid that fails validation at production" ); } } @@ -1211,6 +1229,36 @@ impl BeaconChain { 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, + ctx: &BidRequestContext, + ) -> InclusionListBits { + 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. /// @@ -1224,6 +1272,7 @@ impl BeaconChain { ctx: &BidRequestContext, builder_config: &BuilderConfig, proposer_preferences: &SignedProposerPreferences, + local_inclusion_list_bits: &InclusionListBits, state: &BeaconState, ) -> Vec> { let Some(builders) = self.builders.as_ref() else { @@ -1246,6 +1295,7 @@ impl BeaconChain { 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. @@ -1264,6 +1314,7 @@ impl BeaconChain { 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 @@ -1290,6 +1341,7 @@ impl BeaconChain { executed_ancestor_gas_limit, &expected_builder_pubkeys, &proposer_preferences, + &local_inclusion_list_bits, &state, &spec, ) diff --git a/beacon_node/beacon_chain/src/inclusion_list_store.rs b/beacon_node/beacon_chain/src/inclusion_list_store.rs index f585724beca..c0fdd3eea24 100644 --- a/beacon_node/beacon_chain/src/inclusion_list_store.rs +++ b/beacon_node/beacon_chain/src/inclusion_list_store.rs @@ -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; @@ -63,6 +66,19 @@ pub struct InclusionListStore { _phantom: PhantomData, } +/// Returns whether `bits` is inclusive of `local`: every bit set in `local` is also set in `bits`. +pub fn inclusion_list_bits_are_inclusive( + local: &InclusionListBits, + bits: &InclusionListBits, +) -> Result { + for i in 0..local.len() { + if local.get(i)? && !bits.get(i)? { + return Ok(false); + } + } + Ok(true) +} + impl InclusionListStore { pub fn new(spec: &ChainSpec) -> Self { // `heze_fork_epoch` holds the far future sentinel when Heze is unscheduled. @@ -211,10 +227,10 @@ impl InclusionListStore { dependent_root: DependentRoot, il_committee: &InclusionListCommittee, only_timely: bool, - ) -> Result, Error> { + ) -> Result, Error> { let submitted = self.submitted_validators(slot, dependent_root, only_timely); - let mut bits = BitVector::new(); + let mut bits = InclusionListBits::::new(); for (i, validator) in il_committee.iter().enumerate() { if submitted.contains(validator) { bits.set(i, true)?; @@ -230,17 +246,12 @@ impl InclusionListStore { slot: Slot, dependent_root: DependentRoot, il_committee: &InclusionListCommittee, - bits: &BitVector, + bits: &InclusionListBits, only_timely: bool, ) -> Result { 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::(&local, bits) } /// The stored signed inclusion lists for the given `validators`, used to serve diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs index a063b9c3b0e..295177450f8 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs @@ -1,14 +1,17 @@ use crate::payload_bid_verification::{ PayloadBidError, - gossip_verified_bid::{is_gas_limit_target_compatible, verify_direct_bid_consistency}, + gossip_verified_bid::{ + is_gas_limit_target_compatible, verify_bid_inclusion_list_bits, + verify_direct_bid_consistency, + }, }; use eth2::types::BuilderPubkeys; use state_processing::signature_sets::{ execution_payload_bid_signature_set, get_builder_pubkey_from_state, }; use types::{ - BeaconState, ChainSpec, EthSpec, ExecutionBlockHash, Hash256, SignedExecutionPayloadBid, - SignedProposerPreferences, Slot, + BeaconState, ChainSpec, EthSpec, ExecutionBlockHash, Hash256, InclusionListBits, + SignedExecutionPayloadBid, SignedProposerPreferences, Slot, }; /// Fully validate a bid fetched directly from a builder, for inclusion in a block being produced. @@ -20,7 +23,10 @@ use types::{ /// covers the bid value), /// - that the bid matches the block being produced — the exact `proposal_slot`, the selected /// FULL/EMPTY parent (`parent_block_hash` / `parent_block_root`), the state's RANDAO mix, and a -/// gas limit compatible with the parent's under the proposer's target, and +/// gas limit compatible with the parent's under the proposer's target, +/// - starting with Heze slots, that its `inclusion_list_bits` cover `local_inclusion_list_bits`, +/// the node's own view of the inclusion lists for the slot before `proposal_slot`, considering +/// both timely and untimely inclusion lists, and /// - a valid builder signature. /// /// Unlike gossip bids, direct bids may carry an execution payment; the `execution_payment == 0` @@ -42,6 +48,7 @@ pub fn verify_direct_bid( executed_ancestor_gas_limit: u64, expected_builder_pubkeys: &BuilderPubkeys, proposer_preferences: &SignedProposerPreferences, + local_inclusion_list_bits: &InclusionListBits, state: &BeaconState, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { @@ -88,6 +95,9 @@ pub fn verify_direct_bid( return Err(PayloadBidError::InvalidGasLimit); } + // The bid must cover every inclusion list this node holds for the slot before the proposal. + verify_bid_inclusion_list_bits(bid, local_inclusion_list_bits)?; + // Consensus-consistency checks shared with the gossip verifier. verify_direct_bid_consistency(bid, proposal_slot, proposer_preferences, state, spec)?; @@ -209,6 +219,7 @@ mod tests { EXECUTED_ANCESTOR_GAS_LIMIT, &BuilderPubkeys::default(), &preferences(), + &inclusion_list_bits(&[]), &state, &spec, ); @@ -235,6 +246,7 @@ mod tests { EXECUTED_ANCESTOR_GAS_LIMIT, &BuilderPubkeys::default(), &preferences(), + &inclusion_list_bits(&[]), &state, &spec, ); @@ -261,6 +273,7 @@ mod tests { EXECUTED_ANCESTOR_GAS_LIMIT, &BuilderPubkeys::default(), &preferences(), + &inclusion_list_bits(&[]), &state, &spec, ); @@ -288,6 +301,7 @@ mod tests { EXECUTED_ANCESTOR_GAS_LIMIT, &BuilderPubkeys::default(), &preferences(), + &inclusion_list_bits(&[]), &state, &spec, ); @@ -321,6 +335,7 @@ mod tests { EXECUTED_ANCESTOR_GAS_LIMIT, &BuilderPubkeys::default(), &preferences(), + &inclusion_list_bits(&[]), &state, &spec, ); @@ -351,6 +366,7 @@ mod tests { EXECUTED_ANCESTOR_GAS_LIMIT, &BuilderPubkeys::default(), &preferences(), + &inclusion_list_bits(&[]), &state, &spec, ); @@ -381,6 +397,7 @@ mod tests { EXECUTED_ANCESTOR_GAS_LIMIT, &BuilderPubkeys::default(), &preferences(), + &inclusion_list_bits(&[]), &state, &spec, ); @@ -389,6 +406,81 @@ mod tests { "expected to fail after the gas check, got {result:?}" ); } + + fn inclusion_list_bits(positions: &[usize]) -> InclusionListBits { + let mut bits = InclusionListBits::::new(); + for position in positions { + bits.set(*position, true) + .expect("position within committee size"); + } + bits + } + + /// A Heze bid at slot 1 that passes every check ahead of the inclusion list one. + fn signed_heze_bid(inclusion_list_bits: InclusionListBits) -> SignedExecutionPayloadBid { + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: Slot::new(1), + gas_limit: EXECUTED_ANCESTOR_GAS_LIMIT, + // A default (zero) `block_hash` would equal the zero parent hash and trip the + // block-hash-equals-parent rejection before the checks these tests target. + block_hash: ExecutionBlockHash::repeat_byte(1), + inclusion_list_bits, + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }) + } + + #[test] + fn rejects_inclusion_list_bits_not_covering_local_view() { + let (state, mut spec) = state_and_spec(); + spec.heze_fork_epoch = Some(Epoch::new(0)); + // The node holds lists from committee positions 0, 1 and 2; + // the bid only claims a subset of it + let bid = signed_heze_bid(inclusion_list_bits(&[0, 1])); + let result = verify_direct_bid( + &bid, + Slot::new(1), + ExecutionBlockHash::zero(), + Hash256::ZERO, + EXECUTED_ANCESTOR_GAS_LIMIT, + &BuilderPubkeys::default(), + &preferences(), + &inclusion_list_bits(&[0, 1, 2]), + &state, + &spec, + ); + assert!(matches!( + result, + Err(PayloadBidError::InclusionListBitsNotInclusive { slot }) if slot == Slot::new(1) + )); + } + + #[test] + fn inclusion_list_bits_covering_local_view_pass_inclusivity_check() { + let (state, mut spec) = state_and_spec(); + spec.heze_fork_epoch = Some(Epoch::new(0)); + // The bid claims a superset of the node's view + let bid = signed_heze_bid(inclusion_list_bits(&[0, 1, 2, 5])); + let result = verify_direct_bid( + &bid, + Slot::new(1), + ExecutionBlockHash::zero(), + Hash256::ZERO, + EXECUTED_ANCESTOR_GAS_LIMIT, + &BuilderPubkeys::default(), + &preferences(), + &inclusion_list_bits(&[0, 1, 2]), + &state, + &spec, + ); + assert!( + matches!(result, Err(PayloadBidError::InvalidBuilder { .. })), + "expected to fail after the inclusion list check, got {result:?}" + ); + } + #[test] fn rejects_gloas_bid_at_heze_slot() { let (state, mut spec) = state_and_spec(); @@ -409,6 +501,7 @@ mod tests { EXECUTED_ANCESTOR_GAS_LIMIT, &BuilderPubkeys::default(), &preferences(), + &inclusion_list_bits(&[]), &state, &spec, ); @@ -434,6 +527,7 @@ mod tests { EXECUTED_ANCESTOR_GAS_LIMIT, &BuilderPubkeys::default(), &preferences(), + &inclusion_list_bits(&[]), &state, &spec, ); diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs index 74cfb08c79c..99fc6150050 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use crate::inclusion_list_store::inclusion_list_bits_are_inclusive; use crate::{ BeaconChain, BeaconChainTypes, BeaconStore, CachedHead, CanonicalHead, canonical_head::ForkChoiceReadGuard, @@ -20,7 +21,7 @@ use state_processing::signature_sets::{ use tracing::debug; use types::{ BeaconState, Builder, ChainSpec, EthSpec, ExecutionPayloadBidRef, ExecutionRequestsGloas, - SignedExecutionPayloadBid, SignedProposerPreferences, Slot, + InclusionListBits, SignedExecutionPayloadBid, SignedProposerPreferences, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; @@ -83,6 +84,35 @@ fn verify_bid_blobs( Ok(()) } +/// Reject a bid whose `inclusion_list_bits` do not cover `local_inclusion_list_bits`, the node's +/// own view of the inclusion lists for the slot before the bid's. +/// Pre-Heze bids carry no bits and pass. +/// +/// Callers resolve `local_inclusion_list_bits` under their own timeliness rule. +/// On gossip only timely inclusion lists count, while for the proposer's block production path +/// both timely and untimely inclusion lists count. +pub(crate) fn verify_bid_inclusion_list_bits( + bid: ExecutionPayloadBidRef, + local_inclusion_list_bits: &InclusionListBits, +) -> Result<(), PayloadBidError> { + let ExecutionPayloadBidRef::Heze(bid) = bid else { + return Ok(()); + }; + + let inclusion_list_inclusive = + inclusion_list_bits_are_inclusive::(local_inclusion_list_bits, &bid.inclusion_list_bits) + .map_err(|e| { + PayloadBidError::InternalError(format!( + "inclusion list bits inclusivity check error: {e:?}" + )) + })?; + + if !inclusion_list_inclusive { + return Err(PayloadBidError::InclusionListBitsNotInclusive { slot: bid.slot }); + } + Ok(()) +} + /// Verify that a direct (builder-API) bid is consistent with the current chain state /// and proposer preferences: the direct path's bundle of the shared bid checks. /// diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs index 935db42a15b..1086f66be30 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -92,6 +92,9 @@ pub enum PayloadBidError { }, /// The bids prev randao value is invalid InvalidPrevRandao { slot: Slot }, + /// The bid's `inclusion_list_bits` do not cover every inclusion list the node holds + /// for `slot - 1` + InclusionListBitsNotInclusive { slot: Slot }, /// Some Beacon State error BeaconStateError(BeaconStateError), /// The bid's variant does not match the fork at its slot. diff --git a/beacon_node/beacon_chain/tests/block_production.rs b/beacon_node/beacon_chain/tests/block_production.rs index a5e92f5cb23..56ae6eaa947 100644 --- a/beacon_node/beacon_chain/tests/block_production.rs +++ b/beacon_node/beacon_chain/tests/block_production.rs @@ -2,10 +2,12 @@ use beacon_chain::{ observed_operations::ObservationOutcome, test_utils::{BeaconChainHarness, test_spec}, }; +use bls::Signature; use std::sync::Arc; use types::{ - Address, Epoch, ExecutionRequests, ExecutionRequestsGloas, Hash256, MinimalEthSpec, Slot, - WithdrawalRequest, + Address, Epoch, ExecutionRequests, ExecutionRequestsGloas, Hash256, InclusionList, + MinimalEthSpec, ProgressiveTransactions, SignedExecutionPayloadBidRef, SignedInclusionList, + Slot, WithdrawalRequest, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; type E = MinimalEthSpec; @@ -130,3 +132,81 @@ async fn gloas_block_production_filters_exits_with_parent_partial_withdrawals() spec.far_future_epoch ); } + +/// The self-built bid claims every inclusion list the node holds for the slot before the proposal, +/// both timely and untimely. +#[tokio::test] +async fn heze_self_build_bid_claims_held_inclusion_lists() { + let spec = test_spec::(); + if !spec.fork_name_at_slot::(Slot::new(0)).heze_enabled() { + return; + } + + let harness = BeaconChainHarness::builder(E::default()) + .spec(Arc::new(spec)) + .deterministic_keypairs(64) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let inclusion_list_slot = Slot::new(0); + let parent_root = harness.head_block_root(); + let (committee, dependent_root) = harness + .chain + .inclusion_list_committee(parent_root, inclusion_list_slot) + .unwrap(); + + let timely_submitter = committee[0]; + let late_submitter = *committee + .iter() + .find(|index| **index != timely_submitter) + .unwrap(); + + for (validator_index, is_timely) in [(timely_submitter, true), (late_submitter, false)] { + harness + .chain + .inclusion_list_store + .write() + .process_inclusion_list( + SignedInclusionList { + message: InclusionList { + slot: inclusion_list_slot, + validator_index, + dependent_root, + transactions: ProgressiveTransactions::default(), + }, + signature: Signature::empty(), + }, + is_timely, + ); + } + + harness.advance_slot(); + let state = harness.get_current_state(); + let (block_contents, _, _) = harness.make_block_with_envelope(state, Slot::new(1)).await; + + let SignedExecutionPayloadBidRef::Heze(signed_bid) = block_contents + .0 + .message() + .body() + .signed_execution_payload_bid() + .unwrap() + else { + panic!("a Heze block should carry a Heze bid"); + }; + assert_eq!(signed_bid.message.builder_index, BUILDER_INDEX_SELF_BUILD); + + // A validator holding several committee positions has all of them set + for (position, validator_index) in committee.iter().enumerate() { + let expected = *validator_index == timely_submitter || *validator_index == late_submitter; + assert_eq!( + signed_bid + .message + .inclusion_list_bits + .get(position) + .unwrap(), + expected, + "unexpected bit at committee position {position}" + ); + } +} diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 30b9b83d74b..470945440f1 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4272,7 +4272,8 @@ impl NetworkBeaconProcessor { | PayloadBidError::BeaconStateError(_) | PayloadBidError::InternalError(_) | PayloadBidError::InvalidBidSlot { .. } - | PayloadBidError::UnableToReadSlot, + | PayloadBidError::UnableToReadSlot + | PayloadBidError::InclusionListBitsNotInclusive { .. }, ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } diff --git a/consensus/types/src/execution/inclusion_list.rs b/consensus/types/src/execution/inclusion_list.rs index ed6739a2460..fd585d1d42a 100644 --- a/consensus/types/src/execution/inclusion_list.rs +++ b/consensus/types/src/execution/inclusion_list.rs @@ -3,12 +3,15 @@ use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -use ssz_types::FixedVector; +use ssz_types::{BitVector, FixedVector}; use tree_hash_derive::TreeHash; /// The inclusion list committee, ordered by committee position. pub type InclusionListCommittee = FixedVector::InclusionListCommitteeSize>; +/// One bit per inclusion list committee member, in committee position order. +pub type InclusionListBits = BitVector<::InclusionListCommitteeSize>; + #[derive(Default, Debug, Clone, Serialize, Encode, Decode, Deserialize, TreeHash, Educe)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[educe(PartialEq, Hash)] diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs index f0d5bfb4e83..edbb76dbb3d 100644 --- a/consensus/types/src/execution/mod.rs +++ b/consensus/types/src/execution/mod.rs @@ -45,7 +45,7 @@ pub use execution_requests::{ ExecutionRequests, ExecutionRequestsElectra, ExecutionRequestsGloas, ExecutionRequestsRef, RequestType, WithdrawalRequests, }; -pub use inclusion_list::{InclusionList, InclusionListCommittee}; +pub use inclusion_list::{InclusionList, InclusionListBits, InclusionListCommittee}; pub use payload::{ AbstractExecPayload, BlindedPayload, BlindedPayloadBellatrix, BlindedPayloadCapella, BlindedPayloadDeneb, BlindedPayloadElectra, BlindedPayloadFulu, BlindedPayloadRef,