Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
491b69f
proto node versioning
hopinheimer Feb 20, 2026
3e3ccba
adding michael commits
michaelsproul Dec 11, 2025
d5c5077
implement scoring mechanisms and plumbing
hopinheimer Feb 24, 2026
e04a8c3
adding tests and payload changes
hopinheimer Feb 26, 2026
6d74723
Merge branch 'unstable' of github.com:sigp/lighthouse into gloas-fc-p…
hopinheimer Feb 26, 2026
eb1b810
fixing test
hopinheimer Feb 26, 2026
59033a5
lint
hopinheimer Feb 26, 2026
e68cc03
vote sanity and genesis epoch fix
hopinheimer Mar 2, 2026
6f6da5b
lint
hopinheimer Mar 2, 2026
275ac11
test fixes
hopinheimer Mar 2, 2026
9c6f25c
fix migration `SszContainer` scripts
hopinheimer Mar 9, 2026
ca1b3eb
Merge branch 'unstable' into gloas-fc-proto
hopinheimer Mar 9, 2026
6fbb931
Merge branch 'unstable' of https://github.com/sigp/lighthouse into gl…
eserilev Mar 13, 2026
5679994
addressing comments
hopinheimer Mar 13, 2026
d89e7f7
Merge branch 'gloas-fc-proto' of github.com:hopinheimer/lighthouse in…
hopinheimer Mar 13, 2026
cf9e463
Merge branch 'unstable' into gloas-fc-proto
hopinheimer Mar 16, 2026
f747696
bitfield for `PTC` votes
hopinheimer Mar 16, 2026
97d1b7b
Merge branch 'gloas-fc-proto' of github.com:hopinheimer/lighthouse in…
hopinheimer Mar 16, 2026
0df749f
completing `should_extend_payload` implementation
hopinheimer Mar 16, 2026
916d9fb
changes
hopinheimer Mar 16, 2026
9ce88ea
addressing comments:
hopinheimer Mar 16, 2026
a7bcf0f
enable ef tests @brech1 commit
hopinheimer Mar 17, 2026
ffec1a1
enable ef tests @brech1 commit
hopinheimer Mar 17, 2026
5aa1192
unstable merge
hopinheimer Mar 17, 2026
46a909b
changes
hopinheimer Mar 17, 2026
ab1305d
Propagate weight to parent's full/empty variants
michaelsproul Mar 19, 2026
cc8466d
fixing recursive calls with caching
hopinheimer Mar 20, 2026
cb35ba6
Merge branch 'unstable' of github.com:sigp/lighthouse into fix-gloas-…
hopinheimer Mar 23, 2026
ce71471
passing ef tests ft. @dapplion
hopinheimer Mar 23, 2026
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

54 changes: 40 additions & 14 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ use execution_layer::{
};
use fixed_bytes::FixedBytesExtended;
use fork_choice::{
AttestationFromBlock, ExecutionStatus, ForkChoice, ForkchoiceUpdateParameters,
InvalidationOperation, PayloadVerificationStatus, ResetPayloadStatuses,
ExecutionStatus, ForkChoice, ForkchoiceUpdateParameters, InvalidationOperation,
PayloadVerificationStatus, ResetPayloadStatuses,
};
use futures::channel::mpsc::Sender;
use itertools::Itertools;
Expand Down Expand Up @@ -1451,7 +1451,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.proto_array()
.heads_descended_from_finalization::<T::EthSpec>(fork_choice.finalized_checkpoint())
.iter()
.map(|node| (node.root, node.slot))
.map(|node| (node.root(), node.slot()))
.collect()
}

Expand Down Expand Up @@ -2280,7 +2280,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.on_attestation(
self.slot()?,
verified.indexed_attestation().to_ref(),
AttestationFromBlock::False,
false,
&self.spec,
)
.map_err(Into::into)
}
Expand Down Expand Up @@ -4773,7 +4774,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {

// The slot of our potential re-org block is always 1 greater than the head block because we
// only attempt single-slot re-orgs.
let head_slot = info.head_node.slot;
let head_slot = info.head_node.slot();
let re_org_block_slot = head_slot + 1;
let fork_choice_slot = info.current_slot;

Expand Down Expand Up @@ -4808,9 +4809,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.fork_name_at_slot::<T::EthSpec>(re_org_block_slot)
.fulu_enabled()
{
info.head_node.current_epoch_shuffling_id
info.head_node.current_epoch_shuffling_id()
} else {
info.head_node.next_epoch_shuffling_id
info.head_node.next_epoch_shuffling_id()
}
.shuffling_decision_block;
let proposer_index = self
Expand Down Expand Up @@ -4839,18 +4840,39 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// If the current slot is already equal to the proposal slot (or we are in the tail end of
// the prior slot), then check the actual weight of the head against the head re-org threshold
// and the actual weight of the parent against the parent re-org threshold.
// Per spec `is_head_weak`: uses get_attestation_score(head, PENDING) which is
// the total weight. Per spec `is_parent_strong`: uses
// get_attestation_score(parent, parent_payload_status) where parent_payload_status
// is determined by the head block's relationship to its parent.
let head_weight = info.head_node.weight();
let parent_weight = if let (Ok(head_payload_status), Ok(parent_v29)) = (
info.head_node.parent_payload_status(),
info.parent_node.as_v29(),
) {
// Post-GLOAS: use the payload-filtered weight matching how the head
// extends from its parent.
match head_payload_status {
proto_array::PayloadStatus::Full => parent_v29.full_payload_weight,
proto_array::PayloadStatus::Empty => parent_v29.empty_payload_weight,
proto_array::PayloadStatus::Pending => info.parent_node.weight(),
}
} else {
// Pre-GLOAS or fork boundary: use total weight.
info.parent_node.weight()
};

let (head_weak, parent_strong) = if fork_choice_slot == re_org_block_slot {
(
info.head_node.weight < info.re_org_head_weight_threshold,
info.parent_node.weight > info.re_org_parent_weight_threshold,
head_weight < info.re_org_head_weight_threshold,
parent_weight > info.re_org_parent_weight_threshold,
)
} else {
(true, true)
};
if !head_weak {
return Err(Box::new(
DoNotReOrg::HeadNotWeak {
head_weight: info.head_node.weight,
head_weight,
re_org_head_weight_threshold: info.re_org_head_weight_threshold,
}
.into(),
Expand All @@ -4859,7 +4881,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
if !parent_strong {
return Err(Box::new(
DoNotReOrg::ParentNotStrong {
parent_weight: info.parent_node.weight,
parent_weight,
re_org_parent_weight_threshold: info.re_org_parent_weight_threshold,
}
.into(),
Expand All @@ -4877,17 +4899,21 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
return Err(Box::new(DoNotReOrg::HeadNotLate.into()));
}

let parent_head_hash = info.parent_node.execution_status.block_hash();
let parent_head_hash = info
.parent_node
.execution_status()
.ok()
.and_then(|execution_status| execution_status.block_hash());
let forkchoice_update_params = ForkchoiceUpdateParameters {
head_root: info.parent_node.root,
head_root: info.parent_node.root(),
head_hash: parent_head_hash,
justified_hash: canonical_forkchoice_params.justified_hash,
finalized_hash: canonical_forkchoice_params.finalized_hash,
};

debug!(
canonical_head = ?head_block_root,
?info.parent_node.root,
parent_root = ?info.parent_node.root(),
slot = %fork_choice_slot,
"Fork choice update overridden"
);
Expand Down
4 changes: 2 additions & 2 deletions beacon_node/beacon_chain/src/block_production/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
})
.ok()?;
drop(proposer_head_timer);
let re_org_parent_block = proposer_head.parent_node.root;
let re_org_parent_block = proposer_head.parent_node.root();

let (state_root, state) = self
.store
Expand All @@ -244,7 +244,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
info!(
weak_head = ?canonical_head,
parent = ?re_org_parent_block,
head_weight = proposer_head.head_node.weight,
head_weight = proposer_head.head_node.weight(),
threshold_weight = proposer_head.re_org_head_weight_threshold,
"Attempting re-org due to weak head"
);
Expand Down
47 changes: 34 additions & 13 deletions beacon_node/beacon_chain/src/block_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ use bls::{PublicKey, PublicKeyBytes};
use educe::Educe;
use eth2::types::{BlockGossip, EventKind};
use execution_layer::PayloadStatus;
pub use fork_choice::{AttestationFromBlock, PayloadVerificationStatus};
pub use fork_choice::PayloadVerificationStatus;
use metrics::TryExt;
use parking_lot::RwLockReadGuard;
use proto_array::Block as ProtoBlock;
Expand Down Expand Up @@ -1666,18 +1666,39 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> {
.get_indexed_attestation(&state, attestation)
.map_err(|e| BlockError::PerBlockProcessingError(e.into_with_index(i)))?;

match fork_choice.on_attestation(
current_slot,
indexed_attestation,
AttestationFromBlock::True,
) {
match fork_choice.on_attestation(current_slot, indexed_attestation, true, &chain.spec) {
Ok(()) => Ok(()),
// Ignore invalid attestations whilst importing attestations from a block. The
// block might be very old and therefore the attestations useless to fork choice.
Err(ForkChoiceError::InvalidAttestation(_)) => Ok(()),
Err(e) => Err(BlockError::BeaconChainError(Box::new(e.into()))),
}?;
}

// Register each payload attestation in the block with fork choice.
if let Ok(payload_attestations) = block.message().body().payload_attestations() {
for (i, payload_attestation) in payload_attestations.iter().enumerate() {
let indexed_payload_attestation = consensus_context
.get_indexed_payload_attestation(&state, payload_attestation, &chain.spec)
.map_err(|e| BlockError::PerBlockProcessingError(e.into_with_index(i)))?;

let ptc = state
.get_ptc(indexed_payload_attestation.data.slot, &chain.spec)
.map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?;

match fork_choice.on_payload_attestation(
current_slot,
indexed_payload_attestation,
true,
&ptc.0,
) {
Ok(()) => Ok(()),
// Ignore invalid payload attestations whilst importing from a block.
Err(ForkChoiceError::InvalidAttestation(_)) => Ok(()),
Err(e) => Err(BlockError::BeaconChainError(Box::new(e.into()))),
}?;
}
}
drop(fork_choice);

Ok(Self {
Expand Down Expand Up @@ -1940,13 +1961,13 @@ fn load_parent<T: BeaconChainTypes, B: AsBlock<T::EthSpec>>(
{
if block.as_block().is_parent_block_full(parent_bid_block_hash) {
// TODO(gloas): loading the envelope here is not very efficient
// TODO(gloas): check parent payload existence prior to this point?
let envelope = chain.store.get_payload_envelope(&root)?.ok_or_else(|| {
BeaconChainError::DBInconsistent(format!(
"Missing envelope for parent block {root:?}",
))
})?;
(StatePayloadStatus::Full, envelope.message.state_root)
if let Some(envelope) = chain.store.get_payload_envelope(&root)? {
(StatePayloadStatus::Full, envelope.message.state_root)
} else {
// The envelope hasn't been stored yet (e.g. genesis block, or payload
// not yet delivered). Fall back to the pending/empty state.
(StatePayloadStatus::Pending, parent_block.state_root())
}
} else {
(StatePayloadStatus::Pending, parent_block.state_root())
}
Expand Down
4 changes: 2 additions & 2 deletions beacon_node/beacon_chain/src/invariants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// Only check blocks that are descendants of the finalized checkpoint.
// Pruned non-canonical fork blocks may linger in the proto-array but
// are legitimately absent from the database.
fc.is_finalized_checkpoint_or_descendant(node.root)
fc.is_finalized_checkpoint_or_descendant(node.root())
})
.map(|node| (node.root, node.slot))
.map(|node| (node.root(), node.slot()))
.collect()
};

Expand Down
21 changes: 16 additions & 5 deletions beacon_node/beacon_chain/src/persisted_fork_choice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,23 @@ use superstruct::superstruct;
use types::Hash256;

// If adding a new version you should update this type alias and fix the breakages.
pub type PersistedForkChoice = PersistedForkChoiceV28;
pub type PersistedForkChoice = PersistedForkChoiceV29;

#[superstruct(
variants(V17, V28),
variants(V17, V28, V29),
variant_attributes(derive(Encode, Decode)),
no_enum
)]
pub struct PersistedForkChoice {
#[superstruct(only(V17))]
pub fork_choice_v17: fork_choice::PersistedForkChoiceV17,
#[superstruct(only(V28))]
pub fork_choice: fork_choice::PersistedForkChoiceV28,
pub fork_choice_v28: fork_choice::PersistedForkChoiceV28,
#[superstruct(only(V29))]
pub fork_choice: fork_choice::PersistedForkChoiceV29,
#[superstruct(only(V17))]
pub fork_choice_store_v17: PersistedForkChoiceStoreV17,
#[superstruct(only(V28))]
#[superstruct(only(V28, V29))]
pub fork_choice_store: PersistedForkChoiceStoreV28,
}

Expand All @@ -47,7 +49,7 @@ macro_rules! impl_store_item {

impl_store_item!(PersistedForkChoiceV17);

impl PersistedForkChoiceV28 {
impl PersistedForkChoiceV29 {
pub fn from_bytes(bytes: &[u8], store_config: &StoreConfig) -> Result<Self, Error> {
let decompressed_bytes = store_config
.decompress_bytes(bytes)
Expand Down Expand Up @@ -78,3 +80,12 @@ impl PersistedForkChoiceV28 {
))
}
}

impl From<PersistedForkChoiceV28> for PersistedForkChoiceV29 {
fn from(v28: PersistedForkChoiceV28) -> Self {
Self {
fork_choice: v28.fork_choice_v28.into(),
fork_choice_store: v28.fork_choice_store,
}
}
}
21 changes: 10 additions & 11 deletions beacon_node/beacon_chain/src/schema_change/migration_schema_v23.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,21 @@ pub fn downgrade_from_v23<T: BeaconChainTypes>(
// Doesn't matter what policy we use for invalid payloads, as our head calculation just
// considers descent from finalization.
let reset_payload_statuses = ResetPayloadStatuses::OnlyWithInvalidPayload;
let fork_choice = ForkChoice::from_persisted(
persisted_fork_choice.fork_choice_v17.try_into()?,
reset_payload_statuses,
fc_store,
&db.spec,
)
.map_err(|e| {
Error::MigrationError(format!("Error loading fork choice from persisted: {e:?}"))
})?;
let persisted_fc_v28: fork_choice::PersistedForkChoiceV28 =
persisted_fork_choice.fork_choice_v17.try_into()?;
let persisted_fc_v29: fork_choice::PersistedForkChoiceV29 = persisted_fc_v28.into();
let fork_choice =
ForkChoice::from_persisted(persisted_fc_v29, reset_payload_statuses, fc_store, &db.spec)
.map_err(|e| {
Error::MigrationError(format!("Error loading fork choice from persisted: {e:?}"))
})?;

let heads = fork_choice
.proto_array()
.heads_descended_from_finalization::<T::EthSpec>(fork_choice.finalized_checkpoint());

let head_roots = heads.iter().map(|node| node.root).collect();
let head_slots = heads.iter().map(|node| node.slot).collect();
let head_roots = heads.iter().map(|node| node.root()).collect();
let head_slots = heads.iter().map(|node| node.slot()).collect();

let persisted_beacon_chain_v22 = PersistedBeaconChainV22 {
_canonical_head_block_root: DUMMY_CANONICAL_HEAD_BLOCK_ROOT,
Expand Down
35 changes: 17 additions & 18 deletions beacon_node/beacon_chain/src/schema_change/migration_schema_v28.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
BeaconChain, BeaconChainTypes, BeaconForkChoiceStore, PersistedForkChoiceStoreV17,
beacon_chain::FORK_CHOICE_DB_KEY,
persisted_fork_choice::{PersistedForkChoiceV17, PersistedForkChoiceV28},
persisted_fork_choice::PersistedForkChoiceV17,
summaries_dag::{DAGStateSummary, StateSummariesDAG},
};
use fork_choice::{ForkChoice, ForkChoiceStore, ResetPayloadStatuses};
Expand Down Expand Up @@ -88,8 +88,11 @@ pub fn upgrade_to_v28<T: BeaconChainTypes>(
// Construct top-level ForkChoice struct using the patched fork choice store, and the converted
// proto array.
let reset_payload_statuses = ResetPayloadStatuses::OnlyWithInvalidPayload;
let persisted_fc_v28: fork_choice::PersistedForkChoiceV28 =
persisted_fork_choice_v17.fork_choice_v17.try_into()?;
let persisted_fc_v29: fork_choice::PersistedForkChoiceV29 = persisted_fc_v28.into();
let fork_choice = ForkChoice::from_persisted(
persisted_fork_choice_v17.fork_choice_v17.try_into()?,
persisted_fc_v29,
reset_payload_statuses,
fc_store,
db.get_chain_spec(),
Expand Down Expand Up @@ -118,26 +121,22 @@ pub fn downgrade_from_v28<T: BeaconChainTypes>(
return Ok(vec![]);
};

// Recreate V28 persisted fork choice, then convert each field back to its V17 version.
let persisted_fork_choice = PersistedForkChoiceV28 {
fork_choice: fork_choice.to_persisted(),
fork_choice_store: fork_choice.fc_store().to_persisted(),
};

let persisted_v29 = fork_choice.to_persisted();
let fc_store_v28 = fork_choice.fc_store().to_persisted();
let justified_balances = fork_choice.fc_store().justified_balances();

// Convert V29 proto_array back to legacy V28 for downgrade.
let persisted_fork_choice_v28 = fork_choice::PersistedForkChoiceV28 {
proto_array_v28: persisted_v29.proto_array.into(),
queued_attestations: persisted_v29.queued_attestations,
};

// 1. Create `proto_array::PersistedForkChoiceV17`.
let fork_choice_v17: fork_choice::PersistedForkChoiceV17 = (
persisted_fork_choice.fork_choice,
justified_balances.clone(),
)
.into();
let fork_choice_v17: fork_choice::PersistedForkChoiceV17 =
(persisted_fork_choice_v28, justified_balances.clone()).into();

let fork_choice_store_v17: PersistedForkChoiceStoreV17 = (
persisted_fork_choice.fork_choice_store,
justified_balances.clone(),
)
.into();
let fork_choice_store_v17: PersistedForkChoiceStoreV17 =
(fc_store_v28, justified_balances.clone()).into();

let persisted_fork_choice_v17 = PersistedForkChoiceV17 {
fork_choice_v17,
Comment thread
hopinheimer marked this conversation as resolved.
Expand Down
Loading
Loading