Remove duplicate state in ProtoArray (#8324)

Part of a fork-choice tech debt clean-up https://github.com/sigp/lighthouse/issues/8325

https://github.com/sigp/lighthouse/issues/7089 (non-finalized checkpoint sync) changes the meaning of the checkpoints inside fork-choice. It turns out that we persist the justified and finalized checkpoints **twice** in fork-choice
1. Inside the fork-choice store
2. Inside the proto-array

There's no reason for 2. except for making the function signature of some methods smallers. It's not consistent with the rest of the crate, because in some functions we pass the external variable of time (current_slot) via args, but then read the finalized checkpoint from the internal state. Passing both variables as args makes fork-choice easier to reason about at the cost of a few extra lines.


  Remove the unnecessary state (`justified_checkpoint`, `finalized_checkpoint`) inside `ProtoArray`, to make it easier to reason about.


Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com>

Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com>
This commit is contained in:
Lion - dapplion
2025-11-12 00:42:17 -03:00
committed by GitHub
parent 11d1f60753
commit 53e73fa376
9 changed files with 204 additions and 82 deletions

View File

@@ -424,8 +424,6 @@ impl ProtoArrayForkChoice {
) -> Result<Self, String> {
let mut proto_array = ProtoArray {
prune_threshold: DEFAULT_PRUNE_THRESHOLD,
justified_checkpoint,
finalized_checkpoint,
nodes: Vec::with_capacity(1),
indices: HashMap::with_capacity(1),
previous_proposer_boost: ProposerBoost::default(),
@@ -449,7 +447,12 @@ impl ProtoArrayForkChoice {
};
proto_array
.on_block::<E>(block, current_slot)
.on_block::<E>(
block,
current_slot,
justified_checkpoint,
finalized_checkpoint,
)
.map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?;
Ok(Self {
@@ -473,9 +476,10 @@ impl ProtoArrayForkChoice {
pub fn process_execution_payload_invalidation<E: EthSpec>(
&mut self,
op: &InvalidationOperation,
finalized_checkpoint: Checkpoint,
) -> Result<(), String> {
self.proto_array
.propagate_execution_payload_invalidation::<E>(op)
.propagate_execution_payload_invalidation::<E>(op, finalized_checkpoint)
.map_err(|e| format!("Failed to process invalid payload: {:?}", e))
}
@@ -499,13 +503,20 @@ impl ProtoArrayForkChoice {
&mut self,
block: Block,
current_slot: Slot,
justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
) -> Result<(), String> {
if block.parent_root.is_none() {
return Err("Missing parent root".to_string());
}
self.proto_array
.on_block::<E>(block, current_slot)
.on_block::<E>(
block,
current_slot,
justified_checkpoint,
finalized_checkpoint,
)
.map_err(|e| format!("process_block_error: {:?}", e))
}
@@ -547,7 +558,12 @@ impl ProtoArrayForkChoice {
*old_balances = new_balances.clone();
self.proto_array
.find_head::<E>(&justified_checkpoint.root, current_slot)
.find_head::<E>(
&justified_checkpoint.root,
current_slot,
justified_checkpoint,
finalized_checkpoint,
)
.map_err(|e| format!("find_head failed: {:?}", e))
}
@@ -884,9 +900,10 @@ impl ProtoArrayForkChoice {
pub fn is_finalized_checkpoint_or_descendant<E: EthSpec>(
&self,
descendant_root: Hash256,
best_finalized_checkpoint: Checkpoint,
) -> bool {
self.proto_array
.is_finalized_checkpoint_or_descendant::<E>(descendant_root)
.is_finalized_checkpoint_or_descendant::<E>(descendant_root, best_finalized_checkpoint)
}
pub fn latest_message(&self, validator_index: usize) -> Option<(Hash256, Epoch)> {
@@ -916,12 +933,21 @@ impl ProtoArrayForkChoice {
self.proto_array.iter_block_roots(block_root)
}
pub fn as_ssz_container(&self) -> SszContainer {
SszContainer::from(self)
pub fn as_ssz_container(
&self,
justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
) -> SszContainer {
SszContainer::from_proto_array(self, justified_checkpoint, finalized_checkpoint)
}
pub fn as_bytes(&self) -> Vec<u8> {
SszContainer::from(self).as_ssz_bytes()
pub fn as_bytes(
&self,
justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
) -> Vec<u8> {
self.as_ssz_container(justified_checkpoint, finalized_checkpoint)
.as_ssz_bytes()
}
pub fn from_bytes(bytes: &[u8], balances: JustifiedBalances) -> Result<Self, String> {
@@ -954,8 +980,12 @@ impl ProtoArrayForkChoice {
}
/// Returns all nodes that have zero children and are descended from the finalized checkpoint.
pub fn heads_descended_from_finalization<E: EthSpec>(&self) -> Vec<&ProtoNode> {
self.proto_array.heads_descended_from_finalization::<E>()
pub fn heads_descended_from_finalization<E: EthSpec>(
&self,
best_finalized_checkpoint: Checkpoint,
) -> Vec<&ProtoNode> {
self.proto_array
.heads_descended_from_finalization::<E>(best_finalized_checkpoint)
}
}
@@ -1125,6 +1155,8 @@ mod test_compute_deltas {
unrealized_finalized_checkpoint: Some(genesis_checkpoint),
},
genesis_slot + 1,
genesis_checkpoint,
genesis_checkpoint,
)
.unwrap();
@@ -1148,6 +1180,8 @@ mod test_compute_deltas {
unrealized_finalized_checkpoint: None,
},
genesis_slot + 1,
genesis_checkpoint,
genesis_checkpoint,
)
.unwrap();
@@ -1161,10 +1195,24 @@ mod test_compute_deltas {
assert!(!fc.is_descendant(finalized_root, not_finalized_desc));
assert!(!fc.is_descendant(finalized_root, unknown));
assert!(fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(finalized_root));
assert!(fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(finalized_desc));
assert!(!fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(not_finalized_desc));
assert!(!fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(unknown));
assert!(fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
finalized_root,
genesis_checkpoint
));
assert!(fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
finalized_desc,
genesis_checkpoint
));
assert!(!fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
not_finalized_desc,
genesis_checkpoint
));
assert!(
!fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
unknown,
genesis_checkpoint
)
);
assert!(!fc.is_descendant(finalized_desc, not_finalized_desc));
assert!(fc.is_descendant(finalized_desc, finalized_desc));
@@ -1260,6 +1308,8 @@ mod test_compute_deltas {
unrealized_finalized_checkpoint: Some(genesis_checkpoint),
},
Slot::from(block.slot),
genesis_checkpoint,
genesis_checkpoint,
)
.unwrap();
};
@@ -1314,29 +1364,34 @@ mod test_compute_deltas {
// Set the finalized checkpoint to finalize the first slot of epoch 1 on
// the canonical chain.
fc.proto_array.finalized_checkpoint = Checkpoint {
let finalized_checkpoint = Checkpoint {
root: finalized_root,
epoch: Epoch::new(1),
};
assert!(
fc.proto_array
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(finalized_root),
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
finalized_root,
finalized_checkpoint
),
"the finalized checkpoint is the finalized checkpoint"
);
assert!(
fc.proto_array
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(get_block_root(
canonical_slot
)),
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
get_block_root(canonical_slot),
finalized_checkpoint
),
"the canonical block is a descendant of the finalized checkpoint"
);
assert!(
!fc.proto_array
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(get_block_root(
non_canonical_slot
)),
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
get_block_root(non_canonical_slot),
finalized_checkpoint
),
"although the non-canonical block is a descendant of the finalized block, \
it's not a descendant of the finalized checkpoint"
);