Skip to content

FOCIL [WIP] #7004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 40 commits into
base: unstable
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
024dbf6
add IL type, gossip kind, and pubsub message
jacobkaufmann Nov 25, 2024
4919a3b
add initial IL gossip verification
jacobkaufmann Nov 27, 2024
aca9fc2
feat: add getInclusionListV1 to engine API client
jacobkaufmann Dec 9, 2024
c5c0879
add getInclusionListV1 capability to electra readiness
jacobkaufmann Dec 9, 2024
ba33650
feat: add beacon committee indices getter method
jacobkaufmann Dec 10, 2024
2b3c602
add HTTP API to retrieve validator IL duties
jacobkaufmann Dec 12, 2024
7125f25
add inclusion list duties mgmt to VC duties service
jacobkaufmann Dec 13, 2024
985382e
add IL service to download IL from EL, sign in VC, and publish via BN
jacobkaufmann Dec 19, 2024
9e9b6b7
Merge branch 'stable' into electra-focil
jacobkaufmann Dec 23, 2024
d88f102
Merge branch 'unstable' into electra-focil
jacobkaufmann Dec 26, 2024
30e9ff2
Merge branch 'unstable' of https://github.com/sigp/lighthouse into el…
eserilev Jan 7, 2025
63a26b1
Linting
eserilev Jan 7, 2025
682f10c
Merge branch 'unstable' into electra-focil
jacobkaufmann Jan 7, 2025
3069b36
Fork choice changes
eserilev Jan 9, 2025
f87f838
Implement get_attester_head logic
eserilev Jan 16, 2025
77551ea
Import validated IL's into IL cache
eserilev Jan 17, 2025
d3368f5
Update fork choice
eserilev Jan 26, 2025
f31c0b6
add focil epoch activation
eserilev Feb 1, 2025
db8d659
Merge unstable
eserilev Feb 1, 2025
54c9b4d
Merge branch 'electra-focil-fork-choice' into electra-focil
eserilev Feb 1, 2025
3c90258
code fmt
eserilev Feb 1, 2025
88afbb3
POST inclusion list endpoint
eserilev Feb 2, 2025
7414f48
add intial focil test
eserilev Feb 4, 2025
51984ab
test progress
eserilev Feb 6, 2025
30136ad
Add gossip verified il test coverage
eserilev Feb 9, 2025
f790c22
fmt
eserilev Feb 9, 2025
7eb040c
Add more testing, additional checks
eserilev Feb 10, 2025
cdbdb52
debugging
eserilev Feb 15, 2025
fac7151
build on electra
eserilev Feb 19, 2025
df0aeb0
resolve merge conflicts
eserilev Feb 22, 2025
43243ea
fix merge
eserilev Feb 23, 2025
fde5131
Merge branch 'unstable' of https://github.com/sigp/lighthouse into el…
eserilev Mar 9, 2025
fe96804
rebase onto fulu
eserilev Mar 9, 2025
411860f
rebase ontol electra
eserilev Mar 12, 2025
77e86b1
Add inclusion list sse event
eserilev Mar 21, 2025
3b86e51
fmt
eserilev Mar 21, 2025
19d43a2
merge unstable
eserilev Mar 26, 2025
7d69922
add better logging
eserilev Mar 27, 2025
7bd50a6
Merge branch 'unstable' of https://github.com/sigp/lighthouse into el…
eserilev Apr 6, 2025
e21b578
resolve merge conflict and migrate il service to new pardigmn
eserilev May 21, 2025
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

201 changes: 201 additions & 0 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ use crate::execution_payload::{get_execution_payload, NotifyExecutionLayer, Prep
use crate::fetch_blobs::EngineGetBlobsOutput;
use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult};
use crate::graffiti_calculator::GraffitiCalculator;
use crate::inclusion_list_verification::GossipInclusionListError;
use crate::inclusion_list_verification::GossipVerifiedInclusionList;
use crate::kzg_utils::reconstruct_blobs;
use crate::light_client_finality_update_verification::{
Error as LightClientFinalityUpdateError, VerifiedLightClientFinalityUpdate,
Expand Down Expand Up @@ -466,6 +468,8 @@ pub struct BeaconChain<T: BeaconChainTypes> {
pub(crate) attester_cache: Arc<AttesterCache>,
/// A cache used when producing attestations whilst the head block is still being imported.
pub early_attester_cache: EarlyAttesterCache<T::EthSpec>,
/// A cache used to store verified/equivocating inclusion lists.
pub inclusion_list_cache: RwLock<InclusionListCache<T::EthSpec>>,
/// Cache gossip verified blocks to serve over ReqResp before they are imported
pub reqresp_pre_import_cache: Arc<RwLock<ReqRespPreImportCache<T::EthSpec>>>,
/// A cache used to keep track of various block timings.
Expand Down Expand Up @@ -1648,6 +1652,55 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
Ok((duties, dependent_root, execution_status))
}

/// Returns the inclusion list duties for the given validator indices.
///
/// The returned `Vec` will have the same length as `validator_indices`, any
/// non-existing/inactive validators will have `None` values.
pub fn validator_inclusion_list_duties(
&self,
validator_indices_pubkeys: &[(usize, PublicKeyBytes)],
epoch: Epoch,
head_block_root: Hash256,
) -> Result<(Vec<Option<InclusionListDuty>>, Hash256), Error> {
// NOTE: we likely need some additional logic to handle cases where the head block root is
// from some prior epoch.
let head_block = self
.canonical_head
.fork_choice_read_lock()
.get_block(&head_block_root)
.ok_or(Error::MissingBeaconBlock(head_block_root))?;

// NOTE: here we reuse the attestation shuffling IDs.
let shuffling_id = BlockShufflingIds {
current: head_block.current_epoch_shuffling_id.clone(),
next: head_block.next_epoch_shuffling_id.clone(),
previous: None,
block_root: head_block.root,
}
.id_for_epoch(epoch)
.ok_or_else(|| Error::InvalidShufflingId {
shuffling_epoch: epoch,
head_block_epoch: head_block.slot.epoch(T::EthSpec::slots_per_epoch()),
})?;
let dependent_root = shuffling_id.shuffling_decision_block;

// We elect to cache the state here, because it should always be the head state
let head_beacon_state =
self.get_state(&head_block.state_root, Some(head_block.slot), true)?;
let Some(head_beacon_state) = head_beacon_state else {
return Err(Error::MissingBeaconState(head_block.root));
};
let duties = validator_indices_pubkeys
.iter()
.map(|(validator_index, pubkey_bytes)| {
head_beacon_state
.get_inclusion_list_duties(*pubkey_bytes, *validator_index, epoch, &self.spec)
.map_err(Error::InclusionListDutiesError)
})
.collect::<Result<Vec<_>, _>>()?;
Ok((duties, dependent_root))
}

pub fn get_aggregated_attestation(
&self,
attestation: AttestationRef<T::EthSpec>,
Expand Down Expand Up @@ -2042,6 +2095,100 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
)?)
}

/// Produce an `InclusionList` that is valid for the given `slot`.
///
/// The produced `InclusionList` will not be valid until it has been signed by exactly one
/// validator that is in the inclusion list committee for `slot` in the canonical chain.
///
/// ## Errors
///
/// May return an error if the `request_slot` is too far behind the head state.
pub async fn produce_inclusion_list(
self: &Arc<Self>,
request_slot: Slot,
) -> Result<Option<InclusionListTransactions<T::EthSpec>>, Error> {
let execution_layer = self
.execution_layer
.clone()
.ok_or(Error::ExecutionLayerMissing)?;

// Load the cached head and the associated block hash and slot.
//
// Use a blocking task since blocking the core executor on the canonical head read lock can
// block the core tokio executor.
let chain = self.clone();
let (head_slot, parent_hash) = self
.spawn_blocking_handle(
move || {
let cached_head = chain.canonical_head.cached_head();
let head_slot = cached_head.head_slot();
// let head_hash = cached_head.head_hash();
if let Ok(execution_payload) = cached_head
.snapshot
.beacon_block
.message()
.execution_payload()
{
(head_slot, Some(execution_payload.parent_hash()))
} else {
(head_slot, None)
}
},
"produce_inclusion_list_head_read",
)
.await?;

// NOTE: not sure how to handle scenario where head hash is `None` i.e. pre-bellatrix, which
// is pre-electra.
let Some(parent_hash) = parent_hash else {
debug!("Failed to fetch parent_hash");
return Ok(None);
};

let current_slot = self.slot()?;
let next_slot = current_slot.safe_add(1)?;

// Don't bother with the inclusion list if the head is not the current slot.
//
// This prevents the routine from running during sync.
if head_slot != current_slot {
debug!(?head_slot, ?current_slot, "Head too old for inclusion list");
return Ok(None);
}

// Don't bother with the inclusion list if the request slot is not the next
// slot.
//
// NOTE: does this represent a critical error? should we return an error here or log crit?
// is this check redundant?
if request_slot != next_slot {
debug!(
?request_slot,
?next_slot,
"Inclusion list request slot not equal to next slot"
);
return Ok(None);
}

debug!(
%parent_hash,
?current_slot,
"Attempt to fetch IL from EL"
);
// Retrieve the inclusion list from the execution layer.
let inclusion_list = execution_layer
.get_inclusion_list(parent_hash.0)
.await
.map_err(|e| Error::ExecutionLayerGetInclusionListFailed(Box::new(e)))?;

debug!(
tx_count = inclusion_list.len(),
"Inclusion list fetched from EL"
);

Ok(Some(inclusion_list))
}

/// Performs the same validation as `Self::verify_unaggregated_attestation_for_gossip`, but for
/// multiple attestations using batch BLS verification. Batch verification can provide
/// significant CPU-time savings compared to individual verification.
Expand Down Expand Up @@ -2234,6 +2381,24 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
})
}

/// Accepts some `inclusion_list` from the network and attempts to verify it, returning `Ok(_)` if
/// it is valid to be (re)broadcast on the gossip network.
pub fn verify_inclusion_list_for_gossip(
&self,
inclusion_list: &SignedInclusionList<T::EthSpec>,
) -> Result<GossipVerifiedInclusionList<T>, GossipInclusionListError> {
metrics::inc_counter(&metrics::INCLUSION_LIST_PROCESSING_REQUESTS);
let _timer =
metrics::start_timer(&metrics::UNAGGREGATED_ATTESTATION_GOSSIP_VERIFICATION_TIMES);

GossipVerifiedInclusionList::verify(inclusion_list, self).inspect(|v| {
metrics::inc_counter(&metrics::INCLUSION_LIST_PROCESSING_SUCCESSES);
if let Some(event_handler) = self.event_handler.as_ref() {
event_handler.register(EventKind::InclusionList(Box::new(v.signed_il.clone())));
}
})
}

/// Accepts some attestation-type object and attempts to verify it in the context of fork
/// choice. If it is valid it is applied to `self.fork_choice`.
///
Expand Down Expand Up @@ -7113,6 +7278,42 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
)
}

pub async fn set_unsatisfied_inclusion_list_block(
self: &Arc<Self>,
slot: Slot,
block_root: Hash256,
) -> Result<(), Error> {
let chain = self.clone();
let fork_choice_result = self
.spawn_blocking_handle(
move || {
chain
.canonical_head
.fork_choice_write_lock()
.on_invalid_inclusion_list_payload(slot, block_root)
},
"invalid_inclusion_list_payload",
)
.await;

// Update fork choice.
if let Err(e) = fork_choice_result {
crit!(
error = ?e,
"Failed to process invalid inclusion list payload"
);
}

Ok(())
}

pub fn on_verified_inclusion_list(&self, signed_il: SignedInclusionList<T::EthSpec>) {
info!("Adding verified inclusion list to the cache");
self.inclusion_list_cache
.write()
.on_inclusion_list(signed_il);
}

pub fn metrics(&self) -> BeaconChainMetrics {
BeaconChainMetrics {
reqresp_pre_import_cache_len: self.reqresp_pre_import_cache.read().len(),
Expand Down
27 changes: 26 additions & 1 deletion beacon_node/beacon_chain/src/beacon_fork_choice_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ use fork_choice::ForkChoiceStore;
use proto_array::JustifiedBalances;
use safe_arith::ArithError;
use ssz_derive::{Decode, Encode};
use std::collections::BTreeSet;
use std::collections::{BTreeSet, HashMap};
use std::marker::PhantomData;
use std::sync::Arc;
use store::{Error as StoreError, HotColdDB, ItemStore};
use superstruct::superstruct;
use tracing::info;
use types::{
AbstractExecPayload, BeaconBlockRef, BeaconState, BeaconStateError, Checkpoint, Epoch, EthSpec,
FixedBytesExtended, Hash256, Slot,
Expand Down Expand Up @@ -140,6 +141,8 @@ pub struct BeaconForkChoiceStore<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<
unrealized_finalized_checkpoint: Checkpoint,
proposer_boost_root: Hash256,
equivocating_indices: BTreeSet<u64>,
inclusion_list_equivocators: HashMap<(Slot, Hash256), BTreeSet<u64>>,
unsatisfied_inclusion_list_blocks: HashMap<Slot, Hash256>,
_phantom: PhantomData<E>,
}

Expand Down Expand Up @@ -189,6 +192,8 @@ where
unrealized_finalized_checkpoint: finalized_checkpoint,
proposer_boost_root: Hash256::zero(),
equivocating_indices: BTreeSet::new(),
inclusion_list_equivocators: HashMap::new(),
unsatisfied_inclusion_list_blocks: HashMap::new(),
_phantom: PhantomData,
})
}
Expand Down Expand Up @@ -227,6 +232,8 @@ where
unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint,
proposer_boost_root: persisted.proposer_boost_root,
equivocating_indices: persisted.equivocating_indices,
inclusion_list_equivocators: HashMap::new(),
unsatisfied_inclusion_list_blocks: HashMap::new(),
_phantom: PhantomData,
})
}
Expand Down Expand Up @@ -344,6 +351,24 @@ where
fn extend_equivocating_indices(&mut self, indices: impl IntoIterator<Item = u64>) {
self.equivocating_indices.extend(indices);
}

fn set_unsatisfied_inclusion_list_block(&mut self, slot: Slot, block_root: Hash256) {
info!(
?slot,
%block_root,
"Set unsatisfied inclusion list block"
);
self.unsatisfied_inclusion_list_blocks
.insert(slot, block_root);
}

fn unsatisfied_inclusion_list_block(&self, slot: Slot) -> Option<&Hash256> {
self.unsatisfied_inclusion_list_blocks.get(&slot)
}

fn unsatisfied_inclusion_list_blocks(&self) -> &HashMap<Slot, Hash256> {
&self.unsatisfied_inclusion_list_blocks
}
}

pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV17;
Expand Down
1 change: 1 addition & 0 deletions beacon_node/beacon_chain/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@ where
validator_pubkey_cache: RwLock::new(validator_pubkey_cache),
attester_cache: <_>::default(),
early_attester_cache: <_>::default(),
inclusion_list_cache: <_>::default(),
reqresp_pre_import_cache: <_>::default(),
light_client_server_cache: LightClientServerCache::new(),
light_client_server_tx: self.light_client_server_tx,
Expand Down
5 changes: 5 additions & 0 deletions beacon_node/beacon_chain/src/canonical_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ impl<E: EthSpec> CachedHead<E> {
self.snapshot.beacon_block.slot()
}

/// Returns the `execution_payload.block_hash` of the block at the head of the beacon chain.
pub fn head_hash(&self) -> Option<ExecutionBlockHash> {
self.head_hash
}

/// Returns the `Fork` from the `BeaconState` at the head of the chain.
pub fn head_fork(&self) -> Fork {
self.snapshot.beacon_state.fork()
Expand Down
9 changes: 8 additions & 1 deletion beacon_node/beacon_chain/src/electra_readiness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
//! transition.

use crate::{BeaconChain, BeaconChainTypes};
use execution_layer::http::{ENGINE_GET_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V4};
use execution_layer::http::{
ENGINE_GET_INCLUSION_LIST_V1, ENGINE_GET_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V4,
};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Duration;
Expand Down Expand Up @@ -98,6 +100,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
missing_methods.push_str(ENGINE_NEW_PAYLOAD_V4);
all_good = false;
}
if !capabilities.get_inclusion_list_v1 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_GET_INCLUSION_LIST_V1);
all_good = false;
}

if all_good {
ElectraReadiness::Ready
Expand Down
2 changes: 2 additions & 0 deletions beacon_node/beacon_chain/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ pub enum BeaconChainError {
shuffling_epoch: Epoch,
},
SyncDutiesError(BeaconStateError),
InclusionListDutiesError(BeaconStateError),
InconsistentForwardsIter {
request_slot: Slot,
slot: Slot,
Expand All @@ -149,6 +150,7 @@ pub enum BeaconChainError {
EngineGetCapabilititesFailed(Box<execution_layer::Error>),
ExecutionLayerGetBlockByNumberFailed(Box<execution_layer::Error>),
ExecutionLayerGetBlockByHashFailed(Box<execution_layer::Error>),
ExecutionLayerGetInclusionListFailed(Box<execution_layer::Error>),
BlockHashMissingFromExecutionLayer(ExecutionBlockHash),
InconsistentPayloadReconstructed {
slot: Slot,
Expand Down
Loading
Loading