mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-30 04:37:13 +00:00
Compare commits
40 Commits
unstable
...
fc-complia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d7eb3cc4d | ||
|
|
0a91b63c25 | ||
|
|
cc55c9ef5e | ||
|
|
d7dcf58257 | ||
|
|
7f43ba77b9 | ||
|
|
60472329e9 | ||
|
|
52115542c1 | ||
|
|
93be133093 | ||
|
|
ece4bc0fa8 | ||
|
|
db6cb5ac32 | ||
|
|
8a3ac10fbc | ||
|
|
b7920318e7 | ||
|
|
9fe6f3f68c | ||
|
|
c30dd6f9c3 | ||
|
|
9929ea0da9 | ||
|
|
dc4c4d31dc | ||
|
|
b5a093d2ed | ||
|
|
a3bf75e9a1 | ||
|
|
8ec0c4fe7e | ||
|
|
4f2b393edb | ||
|
|
26098446db | ||
|
|
0fa823af1b | ||
|
|
bd1a9ea2df | ||
|
|
9a0c3f859d | ||
|
|
fd56cc81c6 | ||
|
|
c03c045f8b | ||
|
|
c65ed44d50 | ||
|
|
d4eb6e4727 | ||
|
|
c87522411c | ||
|
|
90fcc51216 | ||
|
|
a795451cf9 | ||
|
|
715d6bfa0c | ||
|
|
e9ae5babc8 | ||
|
|
e0ba04d198 | ||
|
|
d77c23bcfb | ||
|
|
14a4de41e0 | ||
|
|
df9399e957 | ||
|
|
ed4de4ee52 | ||
|
|
6e2a27cc71 | ||
|
|
8eed94e64a |
@@ -278,6 +278,8 @@ pub struct QueuedAttestation {
|
|||||||
target_epoch: Epoch,
|
target_epoch: Epoch,
|
||||||
/// Per Gloas spec: `payload_present = attestation.data.index == 1`.
|
/// Per Gloas spec: `payload_present = attestation.data.index == 1`.
|
||||||
payload_present: bool,
|
payload_present: bool,
|
||||||
|
/// Pre-Gloas latest messages update by target epoch. Gloas updates by slot.
|
||||||
|
update_by_slot: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Legacy queued attestation without payload_present (pre-Gloas, schema V28).
|
/// Legacy queued attestation without payload_present (pre-Gloas, schema V28).
|
||||||
@@ -289,14 +291,18 @@ pub struct QueuedAttestationV28 {
|
|||||||
target_epoch: Epoch,
|
target_epoch: Epoch,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, E: EthSpec> From<IndexedAttestationRef<'a, E>> for QueuedAttestation {
|
impl QueuedAttestation {
|
||||||
fn from(a: IndexedAttestationRef<'a, E>) -> Self {
|
fn from_indexed_attestation<E: EthSpec>(
|
||||||
|
a: IndexedAttestationRef<'_, E>,
|
||||||
|
update_by_slot: bool,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
slot: a.data().slot,
|
slot: a.data().slot,
|
||||||
attesting_indices: a.attesting_indices_to_vec(),
|
attesting_indices: a.attesting_indices_to_vec(),
|
||||||
block_root: a.data().beacon_block_root,
|
block_root: a.data().beacon_block_root,
|
||||||
target_epoch: a.data().target.epoch,
|
target_epoch: a.data().target.epoch,
|
||||||
payload_present: a.data().index == 1,
|
payload_present: update_by_slot && a.data().index == 1,
|
||||||
|
update_by_slot,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -765,18 +771,30 @@ where
|
|||||||
) -> Result<(), Error<T::Error>> {
|
) -> Result<(), Error<T::Error>> {
|
||||||
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_ON_BLOCK_TIMES);
|
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_ON_BLOCK_TIMES);
|
||||||
|
|
||||||
// If this block has already been processed we do not need to reprocess it.
|
|
||||||
// We check this immediately in case re-processing the block mutates some property of the
|
|
||||||
// global fork choice store, e.g. the justified checkpoints or the proposer boost root.
|
|
||||||
if self.proto_array.contains_block(&block_root) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide the slot (as per the system clock) to the `fc_store` and then return its view of
|
// Provide the slot (as per the system clock) to the `fc_store` and then return its view of
|
||||||
// the current slot. The `fc_store` will ensure that the `current_slot` is never
|
// the current slot. The `fc_store` will ensure that the `current_slot` is never
|
||||||
// decreasing, a property which we must maintain.
|
// decreasing, a property which we must maintain.
|
||||||
let current_slot = self.update_time(system_time_current_slot)?;
|
let current_slot = self.update_time(system_time_current_slot)?;
|
||||||
|
|
||||||
|
// Check block is later than the finalized epoch slot (optimization to reduce calls to
|
||||||
|
// get_ancestor).
|
||||||
|
let finalized_slot =
|
||||||
|
compute_start_slot_at_epoch::<E>(self.fc_store.finalized_checkpoint().epoch);
|
||||||
|
if block.slot() <= finalized_slot {
|
||||||
|
return Err(Error::InvalidBlock(InvalidBlock::FinalizedSlot {
|
||||||
|
finalized_slot,
|
||||||
|
block_slot: block.slot(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this block has already been processed we do not need to reprocess it.
|
||||||
|
// We check this before parent lookup and state updates in case re-processing the block
|
||||||
|
// mutates some property of the global fork choice store, e.g. the justified checkpoints or
|
||||||
|
// the proposer boost root. The finalized-slot check above still applies to match the spec.
|
||||||
|
if self.proto_array.contains_block(&block_root) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// Parent block must be known.
|
// Parent block must be known.
|
||||||
let parent_block = self
|
let parent_block = self
|
||||||
.proto_array
|
.proto_array
|
||||||
@@ -794,17 +812,6 @@ where
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that block is later than the finalized epoch slot (optimization to reduce calls to
|
|
||||||
// get_ancestor).
|
|
||||||
let finalized_slot =
|
|
||||||
compute_start_slot_at_epoch::<E>(self.fc_store.finalized_checkpoint().epoch);
|
|
||||||
if block.slot() <= finalized_slot {
|
|
||||||
return Err(Error::InvalidBlock(InvalidBlock::FinalizedSlot {
|
|
||||||
finalized_slot,
|
|
||||||
block_slot: block.slot(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check block is a descendant of the finalized block at the checkpoint finalized slot.
|
// Check block is a descendant of the finalized block at the checkpoint finalized slot.
|
||||||
//
|
//
|
||||||
// Note: the specification uses `hash_tree_root(block)` instead of `block.parent_root` for
|
// Note: the specification uses `hash_tree_root(block)` instead of `block.parent_root` for
|
||||||
@@ -823,6 +830,45 @@ where
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let execution_status = if let Ok(execution_payload) = block.body().execution_payload() {
|
||||||
|
let block_hash = execution_payload.block_hash();
|
||||||
|
|
||||||
|
if block_hash == ExecutionBlockHash::zero() {
|
||||||
|
// The block is post-merge-fork, but pre-terminal-PoW block. We don't need to verify
|
||||||
|
// the payload.
|
||||||
|
ExecutionStatus::irrelevant()
|
||||||
|
} else {
|
||||||
|
match payload_verification_status {
|
||||||
|
PayloadVerificationStatus::Verified => ExecutionStatus::Valid(block_hash),
|
||||||
|
PayloadVerificationStatus::Optimistic => {
|
||||||
|
ExecutionStatus::Optimistic(block_hash)
|
||||||
|
}
|
||||||
|
// It would be a logic error to declare a block irrelevant if it has an
|
||||||
|
// execution payload with a non-zero block hash.
|
||||||
|
PayloadVerificationStatus::Irrelevant => {
|
||||||
|
return Err(Error::InvalidPayloadStatus {
|
||||||
|
block_slot: block.slot(),
|
||||||
|
block_root,
|
||||||
|
payload_verification_status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// There is no payload to verify.
|
||||||
|
ExecutionStatus::irrelevant()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (execution_payload_parent_hash, execution_payload_block_hash) =
|
||||||
|
if let Ok(signed_bid) = block.body().signed_execution_payload_bid() {
|
||||||
|
(
|
||||||
|
Some(signed_bid.message.parent_block_hash),
|
||||||
|
Some(signed_bid.message.block_hash),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
let attestation_threshold = spec.get_attestation_due::<E>(block.slot());
|
let attestation_threshold = spec.get_attestation_due::<E>(block.slot());
|
||||||
|
|
||||||
// Add proposer score boost if the block is the first timely block for this slot and its
|
// Add proposer score boost if the block is the first timely block for this slot and its
|
||||||
@@ -956,45 +1002,6 @@ where
|
|||||||
.on_verified_block(block, block_root, state)
|
.on_verified_block(block, block_root, state)
|
||||||
.map_err(Error::AfterBlockFailed)?;
|
.map_err(Error::AfterBlockFailed)?;
|
||||||
|
|
||||||
let execution_status = if let Ok(execution_payload) = block.body().execution_payload() {
|
|
||||||
let block_hash = execution_payload.block_hash();
|
|
||||||
|
|
||||||
if block_hash == ExecutionBlockHash::zero() {
|
|
||||||
// The block is post-merge-fork, but pre-terminal-PoW block. We don't need to verify
|
|
||||||
// the payload.
|
|
||||||
ExecutionStatus::irrelevant()
|
|
||||||
} else {
|
|
||||||
match payload_verification_status {
|
|
||||||
PayloadVerificationStatus::Verified => ExecutionStatus::Valid(block_hash),
|
|
||||||
PayloadVerificationStatus::Optimistic => {
|
|
||||||
ExecutionStatus::Optimistic(block_hash)
|
|
||||||
}
|
|
||||||
// It would be a logic error to declare a block irrelevant if it has an
|
|
||||||
// execution payload with a non-zero block hash.
|
|
||||||
PayloadVerificationStatus::Irrelevant => {
|
|
||||||
return Err(Error::InvalidPayloadStatus {
|
|
||||||
block_slot: block.slot(),
|
|
||||||
block_root,
|
|
||||||
payload_verification_status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// There is no payload to verify.
|
|
||||||
ExecutionStatus::irrelevant()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (execution_payload_parent_hash, execution_payload_block_hash) =
|
|
||||||
if let Ok(signed_bid) = block.body().signed_execution_payload_bid() {
|
|
||||||
(
|
|
||||||
Some(signed_bid.message.parent_block_hash),
|
|
||||||
Some(signed_bid.message.block_hash),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
// This does not apply a vote to the block, it just makes fork choice aware of the block so
|
// This does not apply a vote to the block, it just makes fork choice aware of the block so
|
||||||
// it can still be identified as the head even if it doesn't have any votes.
|
// it can still be identified as the head even if it doesn't have any votes.
|
||||||
self.proto_array.process_block::<E>(
|
self.proto_array.process_block::<E>(
|
||||||
@@ -1283,10 +1290,10 @@ where
|
|||||||
self.validate_on_attestation(attestation, is_from_block, spec)?;
|
self.validate_on_attestation(attestation, is_from_block, spec)?;
|
||||||
|
|
||||||
// Per Gloas spec: `payload_present = attestation.data.index == 1`.
|
// Per Gloas spec: `payload_present = attestation.data.index == 1`.
|
||||||
let payload_present = spec
|
let is_gloas = spec
|
||||||
.fork_name_at_slot::<E>(attestation.data().slot)
|
.fork_name_at_slot::<E>(attestation.data().slot)
|
||||||
.gloas_enabled()
|
.gloas_enabled();
|
||||||
&& attestation.data().index == 1;
|
let payload_present = is_gloas && attestation.data().index == 1;
|
||||||
|
|
||||||
if attestation.data().slot < self.fc_store.get_current_slot() {
|
if attestation.data().slot < self.fc_store.get_current_slot() {
|
||||||
for validator_index in attestation.attesting_indices_iter() {
|
for validator_index in attestation.attesting_indices_iter() {
|
||||||
@@ -1295,6 +1302,8 @@ where
|
|||||||
attestation.data().beacon_block_root,
|
attestation.data().beacon_block_root,
|
||||||
attestation.data().slot,
|
attestation.data().slot,
|
||||||
payload_present,
|
payload_present,
|
||||||
|
is_gloas,
|
||||||
|
E::slots_per_epoch(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1305,7 +1314,10 @@ where
|
|||||||
// Delay consideration in the fork choice until their slot is in the past.
|
// Delay consideration in the fork choice until their slot is in the past.
|
||||||
// ```
|
// ```
|
||||||
self.queued_attestations
|
self.queued_attestations
|
||||||
.push(QueuedAttestation::from(attestation));
|
.push(QueuedAttestation::from_indexed_attestation(
|
||||||
|
attestation,
|
||||||
|
is_gloas,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1507,6 +1519,8 @@ where
|
|||||||
attestation.block_root,
|
attestation.block_root,
|
||||||
attestation.slot,
|
attestation.slot,
|
||||||
attestation.payload_present,
|
attestation.payload_present,
|
||||||
|
attestation.update_by_slot,
|
||||||
|
E::slots_per_epoch(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1921,6 +1935,7 @@ mod tests {
|
|||||||
block_root: Hash256::zero(),
|
block_root: Hash256::zero(),
|
||||||
target_epoch: Epoch::new(0),
|
target_epoch: Epoch::new(0),
|
||||||
payload_present: false,
|
payload_present: false,
|
||||||
|
update_by_slot: false,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,7 +338,14 @@ impl ForkChoiceTestDefinition {
|
|||||||
attestation_slot,
|
attestation_slot,
|
||||||
} => {
|
} => {
|
||||||
fork_choice
|
fork_choice
|
||||||
.process_attestation(validator_index, block_root, attestation_slot, false)
|
.process_attestation(
|
||||||
|
validator_index,
|
||||||
|
block_root,
|
||||||
|
attestation_slot,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
MainnetEthSpec::slots_per_epoch(),
|
||||||
|
)
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| {
|
||||||
panic!(
|
panic!(
|
||||||
"process_attestation op at index {} returned error",
|
"process_attestation op at index {} returned error",
|
||||||
@@ -359,6 +366,8 @@ impl ForkChoiceTestDefinition {
|
|||||||
block_root,
|
block_root,
|
||||||
attestation_slot,
|
attestation_slot,
|
||||||
payload_present,
|
payload_present,
|
||||||
|
true,
|
||||||
|
MainnetEthSpec::slots_per_epoch(),
|
||||||
)
|
)
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| {
|
||||||
panic!(
|
panic!(
|
||||||
|
|||||||
@@ -717,7 +717,7 @@ impl ProtoArray {
|
|||||||
/// Returns `true` if the proposer boost should be kept. Returns `false` if the
|
/// Returns `true` if the proposer boost should be kept. Returns `false` if the
|
||||||
/// boost should be subtracted (invalidated) because the parent is weak and there
|
/// boost should be subtracted (invalidated) because the parent is weak and there
|
||||||
/// are no equivocating blocks at the parent's slot.
|
/// are no equivocating blocks at the parent's slot.
|
||||||
fn should_apply_proposer_boost<E: EthSpec>(
|
pub(crate) fn should_apply_proposer_boost<E: EthSpec>(
|
||||||
&self,
|
&self,
|
||||||
proposer_boost_root: Hash256,
|
proposer_boost_root: Hash256,
|
||||||
justified_balances: &JustifiedBalances,
|
justified_balances: &JustifiedBalances,
|
||||||
@@ -1122,7 +1122,7 @@ impl ProtoArray {
|
|||||||
///
|
///
|
||||||
/// Returns the set of node indices on viable branches — those with at least
|
/// Returns the set of node indices on viable branches — those with at least
|
||||||
/// one leaf descendant with correct justified/finalized checkpoints.
|
/// one leaf descendant with correct justified/finalized checkpoints.
|
||||||
fn get_filtered_block_tree<E: EthSpec>(
|
pub(crate) fn get_filtered_block_tree<E: EthSpec>(
|
||||||
&self,
|
&self,
|
||||||
start_index: usize,
|
start_index: usize,
|
||||||
current_slot: Slot,
|
current_slot: Slot,
|
||||||
@@ -1360,7 +1360,7 @@ impl ProtoArray {
|
|||||||
|
|
||||||
/// Spec: `get_weight`.
|
/// Spec: `get_weight`.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn get_weight<E: EthSpec>(
|
pub(crate) fn get_weight<E: EthSpec>(
|
||||||
&self,
|
&self,
|
||||||
fc_node: &IndexedForkChoiceNode,
|
fc_node: &IndexedForkChoiceNode,
|
||||||
proto_node: &ProtoNode,
|
proto_node: &ProtoNode,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use ssz::{Decode, Encode};
|
use ssz::{Decode, Encode};
|
||||||
use ssz_derive::{Decode, Encode};
|
use ssz_derive::{Decode, Encode};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeSet, HashMap},
|
collections::{BTreeSet, HashMap, HashSet},
|
||||||
fmt,
|
fmt,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -594,10 +594,18 @@ impl ProtoArrayForkChoice {
|
|||||||
block_root: Hash256,
|
block_root: Hash256,
|
||||||
attestation_slot: Slot,
|
attestation_slot: Slot,
|
||||||
payload_present: bool,
|
payload_present: bool,
|
||||||
|
update_by_slot: bool,
|
||||||
|
slots_per_epoch: u64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let vote = self.votes.get_mut(validator_index);
|
let vote = self.votes.get_mut(validator_index);
|
||||||
|
|
||||||
if attestation_slot > vote.next_slot || *vote == VoteTracker::default() {
|
let is_newer_vote = if update_by_slot {
|
||||||
|
attestation_slot > vote.next_slot
|
||||||
|
} else {
|
||||||
|
attestation_slot.epoch(slots_per_epoch) > vote.next_slot.epoch(slots_per_epoch)
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_newer_vote || *vote == VoteTracker::default() {
|
||||||
vote.next_root = block_root;
|
vote.next_root = block_root;
|
||||||
vote.next_slot = attestation_slot;
|
vote.next_slot = attestation_slot;
|
||||||
vote.next_payload_present = payload_present;
|
vote.next_payload_present = payload_present;
|
||||||
@@ -1110,6 +1118,121 @@ impl ProtoArrayForkChoice {
|
|||||||
.map(|node| node.weight())
|
.map(|node| node.weight())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the leaves of the filtered block tree (rooted at `justified_root`) along with
|
||||||
|
/// their weights — i.e. roots that are viable for head and have no descendant that is also
|
||||||
|
/// viable for head. Mirrors the spec's `viable_for_head_roots_and_weights` check.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn filtered_block_tree_leaves_and_weights<E: EthSpec>(
|
||||||
|
&self,
|
||||||
|
justified_root: &Hash256,
|
||||||
|
current_slot: Slot,
|
||||||
|
justified_checkpoint: Checkpoint,
|
||||||
|
finalized_checkpoint: Checkpoint,
|
||||||
|
proposer_boost_root: Hash256,
|
||||||
|
justified_balances: &JustifiedBalances,
|
||||||
|
spec: &ChainSpec,
|
||||||
|
) -> Result<Vec<(Hash256, PayloadStatus, u64)>, String> {
|
||||||
|
let start_index = self
|
||||||
|
.proto_array
|
||||||
|
.indices
|
||||||
|
.get(justified_root)
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"filtered_block_tree_leaves_and_weights: justified node \
|
||||||
|
{justified_root:?} unknown"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let viable = self.proto_array.get_filtered_block_tree::<E>(
|
||||||
|
start_index,
|
||||||
|
current_slot,
|
||||||
|
justified_checkpoint,
|
||||||
|
finalized_checkpoint,
|
||||||
|
);
|
||||||
|
let viable_indices = viable.iter().copied().collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let apply_proposer_boost = self
|
||||||
|
.proto_array
|
||||||
|
.should_apply_proposer_boost::<E>(proposer_boost_root, justified_balances, spec)
|
||||||
|
.map_err(|e| format!("should_apply_proposer_boost failed: {e:?}"))?;
|
||||||
|
|
||||||
|
let mut leaves = Vec::new();
|
||||||
|
let mut stack = vec![IndexedForkChoiceNode {
|
||||||
|
root: *justified_root,
|
||||||
|
proto_node_index: start_index,
|
||||||
|
payload_status: PayloadStatus::Pending,
|
||||||
|
}];
|
||||||
|
|
||||||
|
while let Some(fc_node) = stack.pop() {
|
||||||
|
let proto_node = self
|
||||||
|
.proto_array
|
||||||
|
.nodes
|
||||||
|
.get(fc_node.proto_node_index)
|
||||||
|
.ok_or_else(|| format!("invalid viable node index {}", fc_node.proto_node_index))?;
|
||||||
|
|
||||||
|
let children = if proto_node.payload_received().is_ok() {
|
||||||
|
if fc_node.payload_status == PayloadStatus::Pending {
|
||||||
|
let mut children = vec![fc_node.with_status(PayloadStatus::Empty)];
|
||||||
|
if proto_node.payload_received().is_ok_and(|received| received) {
|
||||||
|
children.push(fc_node.with_status(PayloadStatus::Full));
|
||||||
|
}
|
||||||
|
children
|
||||||
|
} else {
|
||||||
|
self.proto_array
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(child_index, child_node)| {
|
||||||
|
viable_indices.contains(child_index)
|
||||||
|
&& child_node.parent() == Some(fc_node.proto_node_index)
|
||||||
|
&& child_node.get_parent_payload_status() == fc_node.payload_status
|
||||||
|
})
|
||||||
|
.map(|(child_index, child_node)| IndexedForkChoiceNode {
|
||||||
|
root: child_node.root(),
|
||||||
|
proto_node_index: child_index,
|
||||||
|
payload_status: PayloadStatus::Pending,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.proto_array
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(child_index, child_node)| {
|
||||||
|
viable_indices.contains(child_index)
|
||||||
|
&& child_node.parent() == Some(fc_node.proto_node_index)
|
||||||
|
})
|
||||||
|
.map(|(child_index, child_node)| IndexedForkChoiceNode {
|
||||||
|
root: child_node.root(),
|
||||||
|
proto_node_index: child_index,
|
||||||
|
payload_status: PayloadStatus::Pending,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
if children.is_empty() {
|
||||||
|
let weight = self
|
||||||
|
.proto_array
|
||||||
|
.get_weight::<E>(
|
||||||
|
&fc_node,
|
||||||
|
proto_node,
|
||||||
|
apply_proposer_boost,
|
||||||
|
proposer_boost_root,
|
||||||
|
current_slot,
|
||||||
|
justified_balances,
|
||||||
|
spec,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("get_weight failed: {e:?}"))?;
|
||||||
|
leaves.push((fc_node.root, fc_node.payload_status, weight));
|
||||||
|
} else {
|
||||||
|
stack.extend(children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(leaves)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the payload status of the head node based on accumulated weights and tiebreaker.
|
/// Returns the payload status of the head node based on accumulated weights and tiebreaker.
|
||||||
///
|
///
|
||||||
/// See `ProtoArray` documentation.
|
/// See `ProtoArray` documentation.
|
||||||
|
|||||||
@@ -10,9 +10,15 @@ BLS_TEST = bls_tests_yaml
|
|||||||
BLS_OUTPUT_DIR := $(OUTPUT_DIR)/$(BLS_TEST_REPO_NAME)
|
BLS_OUTPUT_DIR := $(OUTPUT_DIR)/$(BLS_TEST_REPO_NAME)
|
||||||
BLS_BASE_URL := https://github.com/ethereum/$(BLS_TEST_REPO_NAME)/releases/download/$(BLS_TEST_VERSION)
|
BLS_BASE_URL := https://github.com/ethereum/$(BLS_TEST_REPO_NAME)/releases/download/$(BLS_TEST_VERSION)
|
||||||
|
|
||||||
|
# Fork-choice compliance tests from consensus-specs CI.
|
||||||
|
# Pin to a specific workflow run ID for reproducibility. Update to pull newer vectors.
|
||||||
|
COMPLIANCE_RUN_ID ?= 26135062633
|
||||||
|
COMPLIANCE_PRESET ?= minimal
|
||||||
|
COMPLIANCE_OUTPUT_DIR := $(OUTPUT_DIR)/tests/$(COMPLIANCE_PRESET)/fulu/fork_choice_compliance
|
||||||
|
|
||||||
.PHONY: all clean
|
.PHONY: all clean
|
||||||
|
|
||||||
all: clean $(OUTPUT_DIR) $(BLS_OUTPUT_DIR)
|
all: clean $(OUTPUT_DIR) $(BLS_OUTPUT_DIR) $(COMPLIANCE_OUTPUT_DIR)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf *.tar.gz $(OUTPUT_DIR) $(BLS_OUTPUT_DIR)
|
rm -rf *.tar.gz $(OUTPUT_DIR) $(BLS_OUTPUT_DIR)
|
||||||
@@ -31,3 +37,12 @@ $(BLS_OUTPUT_DIR):
|
|||||||
$(BLS_BASE_URL)/$(BLS_TEST).tar.gz
|
$(BLS_BASE_URL)/$(BLS_TEST).tar.gz
|
||||||
tar -xzf *.tar.gz -C $(BLS_OUTPUT_DIR)
|
tar -xzf *.tar.gz -C $(BLS_OUTPUT_DIR)
|
||||||
rm -f *.tar.gz
|
rm -f *.tar.gz
|
||||||
|
|
||||||
|
$(COMPLIANCE_OUTPUT_DIR):
|
||||||
|
@echo "Fetching fork-choice compliance tests (run $(COMPLIANCE_RUN_ID))..."
|
||||||
|
@curl -L -f -H "Authorization: token $(GITHUB_TOKEN)" \
|
||||||
|
"https://api.github.com/repos/ethereum/consensus-specs/actions/runs/$(COMPLIANCE_RUN_ID)/artifacts" \
|
||||||
|
| python3 -c "import sys,json; arts=json.load(sys.stdin)['artifacts']; url=next(a['archive_download_url'] for a in arts if a['name']=='small.tar.gz'); print(url)" \
|
||||||
|
| xargs curl -L -f --output compliance.tar.gz -H "Authorization: token $(GITHUB_TOKEN)"
|
||||||
|
tar -xzf compliance.tar.gz -C $(OUTPUT_DIR)
|
||||||
|
rm -f compliance.tar.gz
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ excluded_paths = [
|
|||||||
"tests/.*/.*/networking/.*",
|
"tests/.*/.*/networking/.*",
|
||||||
# TODO: fast confirmation rule not merged yet
|
# TODO: fast confirmation rule not merged yet
|
||||||
"tests/.*/.*/fast_confirmation",
|
"tests/.*/.*/fast_confirmation",
|
||||||
|
# TODO: fork choice compliance invalid_message tests not implemented yet
|
||||||
|
"tests/minimal/.*/fork_choice_compliance/invalid_message_test/.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file};
|
use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file};
|
||||||
use ::fork_choice::{AttestationFromBlock, PayloadVerificationStatus, ProposerHeadError};
|
use ::fork_choice::{
|
||||||
|
AttestationFromBlock, ForkChoiceStore, PayloadStatus as FcPayloadStatus,
|
||||||
|
PayloadVerificationStatus, ProposerHeadError,
|
||||||
|
};
|
||||||
use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head;
|
use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head;
|
||||||
use beacon_chain::blob_verification::GossipBlobError;
|
use beacon_chain::blob_verification::GossipBlobError;
|
||||||
use beacon_chain::block_verification_types::LookupBlock;
|
use beacon_chain::block_verification_types::LookupBlock;
|
||||||
@@ -9,9 +12,7 @@ use beacon_chain::data_column_verification::GossipVerifiedDataColumn;
|
|||||||
use beacon_chain::slot_clock::SlotClock;
|
use beacon_chain::slot_clock::SlotClock;
|
||||||
use beacon_chain::{
|
use beacon_chain::{
|
||||||
AvailabilityProcessingStatus, BeaconChainTypes, CachedHead, ChainConfig, NotifyExecutionLayer,
|
AvailabilityProcessingStatus, BeaconChainTypes, CachedHead, ChainConfig, NotifyExecutionLayer,
|
||||||
attestation_verification::{
|
attestation_verification::VerifiedAttestation,
|
||||||
VerifiedAttestation, obtain_indexed_attestation_and_committees_per_slot,
|
|
||||||
},
|
|
||||||
blob_verification::GossipVerifiedBlob,
|
blob_verification::GossipVerifiedBlob,
|
||||||
custody_context::NodeCustodyType,
|
custody_context::NodeCustodyType,
|
||||||
test_utils::{BeaconChainHarness, EphemeralHarnessType},
|
test_utils::{BeaconChainHarness, EphemeralHarnessType},
|
||||||
@@ -25,8 +26,10 @@ use serde::Deserialize;
|
|||||||
use ssz_derive::Decode;
|
use ssz_derive::Decode;
|
||||||
use ssz_types::VariableList;
|
use ssz_types::VariableList;
|
||||||
use state_processing::VerifySignatures;
|
use state_processing::VerifySignatures;
|
||||||
|
use state_processing::common::{attesting_indices_base, attesting_indices_electra};
|
||||||
use state_processing::envelope_processing::verify_execution_payload_envelope;
|
use state_processing::envelope_processing::verify_execution_payload_envelope;
|
||||||
use state_processing::per_block_processing::is_valid_indexed_payload_attestation;
|
use state_processing::per_block_processing::is_valid_indexed_payload_attestation;
|
||||||
|
use state_processing::per_block_processing::verify_attester_slashing;
|
||||||
use state_processing::state_advance::complete_state_advance;
|
use state_processing::state_advance::complete_state_advance;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -88,6 +91,14 @@ pub struct Checks {
|
|||||||
head_payload_status: Option<u8>,
|
head_payload_status: Option<u8>,
|
||||||
payload_timeliness_vote: Option<PayloadVoteCheck>,
|
payload_timeliness_vote: Option<PayloadVoteCheck>,
|
||||||
payload_data_availability_vote: Option<PayloadVoteCheck>,
|
payload_data_availability_vote: Option<PayloadVoteCheck>,
|
||||||
|
viable_for_head_roots_and_weights: Option<Vec<RootAndWeight>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct RootAndWeight {
|
||||||
|
pub root: Hash256,
|
||||||
|
pub weight: u64,
|
||||||
|
pub payload_status: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
@@ -134,9 +145,13 @@ pub enum Step<
|
|||||||
},
|
},
|
||||||
Attestation {
|
Attestation {
|
||||||
attestation: TAttestation,
|
attestation: TAttestation,
|
||||||
|
#[serde(default)]
|
||||||
|
valid: Option<bool>,
|
||||||
},
|
},
|
||||||
AttesterSlashing {
|
AttesterSlashing {
|
||||||
attester_slashing: TAttesterSlashing,
|
attester_slashing: TAttesterSlashing,
|
||||||
|
#[serde(default)]
|
||||||
|
valid: Option<bool>,
|
||||||
},
|
},
|
||||||
PowBlock {
|
PowBlock {
|
||||||
pow_block: TPowBlock,
|
pow_block: TPowBlock,
|
||||||
@@ -168,11 +183,10 @@ fn default_true() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct Meta {
|
pub struct Meta {
|
||||||
#[serde(rename(deserialize = "description"))]
|
#[serde(rename(deserialize = "description"), default)]
|
||||||
_description: String,
|
_description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -242,31 +256,38 @@ impl<E: EthSpec> LoadCase for ForkChoiceTest<E> {
|
|||||||
valid,
|
valid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Step::Attestation { attestation } => {
|
Step::Attestation { attestation, valid } => {
|
||||||
if fork_name.electra_enabled() {
|
if fork_name.electra_enabled() {
|
||||||
ssz_decode_file(&path.join(format!("{}.ssz_snappy", attestation))).map(
|
ssz_decode_file(&path.join(format!("{}.ssz_snappy", attestation))).map(
|
||||||
|attestation| Step::Attestation {
|
|attestation| Step::Attestation {
|
||||||
attestation: Attestation::Electra(attestation),
|
attestation: Attestation::Electra(attestation),
|
||||||
|
valid,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ssz_decode_file(&path.join(format!("{}.ssz_snappy", attestation))).map(
|
ssz_decode_file(&path.join(format!("{}.ssz_snappy", attestation))).map(
|
||||||
|attestation| Step::Attestation {
|
|attestation| Step::Attestation {
|
||||||
attestation: Attestation::Base(attestation),
|
attestation: Attestation::Base(attestation),
|
||||||
|
valid,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Step::AttesterSlashing { attester_slashing } => {
|
Step::AttesterSlashing {
|
||||||
|
attester_slashing,
|
||||||
|
valid,
|
||||||
|
} => {
|
||||||
if fork_name.electra_enabled() {
|
if fork_name.electra_enabled() {
|
||||||
ssz_decode_file(&path.join(format!("{}.ssz_snappy", attester_slashing)))
|
ssz_decode_file(&path.join(format!("{}.ssz_snappy", attester_slashing)))
|
||||||
.map(|attester_slashing| Step::AttesterSlashing {
|
.map(|attester_slashing| Step::AttesterSlashing {
|
||||||
attester_slashing: AttesterSlashing::Electra(attester_slashing),
|
attester_slashing: AttesterSlashing::Electra(attester_slashing),
|
||||||
|
valid,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ssz_decode_file(&path.join(format!("{}.ssz_snappy", attester_slashing)))
|
ssz_decode_file(&path.join(format!("{}.ssz_snappy", attester_slashing)))
|
||||||
.map(|attester_slashing| Step::AttesterSlashing {
|
.map(|attester_slashing| Step::AttesterSlashing {
|
||||||
attester_slashing: AttesterSlashing::Base(attester_slashing),
|
attester_slashing: AttesterSlashing::Base(attester_slashing),
|
||||||
|
valid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,9 +412,51 @@ impl<E: EthSpec> Case for ForkChoiceTest<E> {
|
|||||||
proofs.clone(),
|
proofs.clone(),
|
||||||
*valid,
|
*valid,
|
||||||
)?,
|
)?,
|
||||||
Step::Attestation { attestation } => tester.process_attestation(attestation)?,
|
Step::Attestation { attestation, valid } => {
|
||||||
Step::AttesterSlashing { attester_slashing } => {
|
let result = tester.process_attestation(attestation);
|
||||||
tester.process_attester_slashing(attester_slashing.to_ref())
|
// Compliance tests use `valid: false` to indicate the attestation is
|
||||||
|
// intentionally malformed and should be rejected. In that case, an error
|
||||||
|
// here is the expected outcome.
|
||||||
|
match valid {
|
||||||
|
Some(false) => {
|
||||||
|
if result.is_ok() {
|
||||||
|
// We allow acceptance of future slot attestations which the spec
|
||||||
|
// deems invalid (we just queue them).
|
||||||
|
let current_slot = tester
|
||||||
|
.harness
|
||||||
|
.chain
|
||||||
|
.canonical_head
|
||||||
|
.fork_choice_read_lock()
|
||||||
|
.fc_store()
|
||||||
|
.get_current_slot();
|
||||||
|
let future_attestation = attestation.data().slot >= current_slot;
|
||||||
|
if !future_attestation {
|
||||||
|
return Err(Error::DidntFail(
|
||||||
|
"attestation marked valid=false should have been rejected"
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(true) | None => result?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Step::AttesterSlashing {
|
||||||
|
attester_slashing,
|
||||||
|
valid,
|
||||||
|
} => {
|
||||||
|
let result = tester.process_attester_slashing(attester_slashing.to_ref());
|
||||||
|
match valid {
|
||||||
|
Some(false) => {
|
||||||
|
if result.is_ok() {
|
||||||
|
return Err(Error::DidntFail(
|
||||||
|
"attester slashing marked valid=false should have been rejected"
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(true) | None => result?,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Step::PowBlock { pow_block } => tester.process_pow_block(pow_block),
|
Step::PowBlock { pow_block } => tester.process_pow_block(pow_block),
|
||||||
Step::OnPayloadInfo {
|
Step::OnPayloadInfo {
|
||||||
@@ -420,6 +483,7 @@ impl<E: EthSpec> Case for ForkChoiceTest<E> {
|
|||||||
head_payload_status,
|
head_payload_status,
|
||||||
payload_timeliness_vote,
|
payload_timeliness_vote,
|
||||||
payload_data_availability_vote,
|
payload_data_availability_vote,
|
||||||
|
viable_for_head_roots_and_weights,
|
||||||
} = checks.as_ref();
|
} = checks.as_ref();
|
||||||
|
|
||||||
if let Some(expected_head) = head {
|
if let Some(expected_head) = head {
|
||||||
@@ -478,6 +542,10 @@ impl<E: EthSpec> Case for ForkChoiceTest<E> {
|
|||||||
if let Some(expected) = payload_data_availability_vote {
|
if let Some(expected) = payload_data_availability_vote {
|
||||||
tester.check_payload_data_availability_vote(expected)?;
|
tester.check_payload_data_availability_vote(expected)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(expected) = viable_for_head_roots_and_weights {
|
||||||
|
tester.check_viable_for_head_roots_and_weights(expected)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Step::MaybeValidBlockAndColumns {
|
Step::MaybeValidBlockAndColumns {
|
||||||
@@ -602,7 +670,6 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
// Compute the slot time manually to ensure the slot clock is correct.
|
// Compute the slot time manually to ensure the slot clock is correct.
|
||||||
let slot = self.tick_to_slot(tick).unwrap();
|
let slot = self.tick_to_slot(tick).unwrap();
|
||||||
assert_eq!(slot, self.harness.chain.slot().unwrap());
|
assert_eq!(slot, self.harness.chain.slot().unwrap());
|
||||||
|
|
||||||
self.harness
|
self.harness
|
||||||
.chain
|
.chain
|
||||||
.canonical_head
|
.canonical_head
|
||||||
@@ -617,6 +684,13 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
columns: Option<DataColumnSidecarList<E>>,
|
columns: Option<DataColumnSidecarList<E>>,
|
||||||
valid: bool,
|
valid: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
// Some fake-crypto EF fixtures contain block attestations that index to different
|
||||||
|
// validators depending on whether they are decoded against the block context or the
|
||||||
|
// attestation target context. Real BLS signatures should make these fixtures impossible.
|
||||||
|
if valid && self.block_attestations_have_divergent_indices(&block)? {
|
||||||
|
return Err(Error::SkippedKnownFailure);
|
||||||
|
}
|
||||||
|
|
||||||
let block_root = block.canonical_root();
|
let block_root = block.canonical_root();
|
||||||
|
|
||||||
let mut data_column_success = true;
|
let mut data_column_success = true;
|
||||||
@@ -655,14 +729,16 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
|| Ok(()),
|
|| Ok(()),
|
||||||
))?
|
))?
|
||||||
.map(|avail: AvailabilityProcessingStatus| avail.try_into());
|
.map(|avail: AvailabilityProcessingStatus| avail.try_into());
|
||||||
let success = data_column_success && result.as_ref().is_ok_and(|inner| inner.is_ok());
|
let is_duplicate = matches!(
|
||||||
|
&result,
|
||||||
|
Err(beacon_chain::BlockError::DuplicateFullyImported(_))
|
||||||
|
);
|
||||||
|
let success = data_column_success
|
||||||
|
&& (result.as_ref().is_ok_and(|inner| inner.is_ok()) || is_duplicate);
|
||||||
if success != valid {
|
if success != valid {
|
||||||
return Err(Error::DidntFail(format!(
|
return Err(Error::DidntFail(format!(
|
||||||
"block with root {} was valid={} whilst test expects valid={}. result: {:?}",
|
"block with root {} was valid={} whilst test expects valid={}. result: {:?}",
|
||||||
block_root,
|
block_root, success, valid, result
|
||||||
result.is_ok(),
|
|
||||||
valid,
|
|
||||||
result
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,18 +746,6 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
self.apply_invalid_block(&block)?;
|
self.apply_invalid_block(&block)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per spec test runner: an on_block step implies receiving block's attestations
|
|
||||||
// and attester slashings.
|
|
||||||
if success {
|
|
||||||
for attestation in block.message().body().attestations() {
|
|
||||||
let att = attestation.clone_as_attestation();
|
|
||||||
let _ = self.process_attestation(&att);
|
|
||||||
}
|
|
||||||
for attester_slashing in block.message().body().attester_slashings() {
|
|
||||||
self.process_attester_slashing(attester_slashing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,6 +756,13 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
kzg_proofs: Option<Vec<KzgProof>>,
|
kzg_proofs: Option<Vec<KzgProof>>,
|
||||||
valid: bool,
|
valid: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
// Some fake-crypto EF fixtures contain block attestations that index to different
|
||||||
|
// validators depending on whether they are decoded against the block context or the
|
||||||
|
// attestation target context. Real BLS signatures should make these fixtures impossible.
|
||||||
|
if valid && self.block_attestations_have_divergent_indices(&block)? {
|
||||||
|
return Err(Error::SkippedKnownFailure);
|
||||||
|
}
|
||||||
|
|
||||||
let block_root = block.canonical_root();
|
let block_root = block.canonical_root();
|
||||||
|
|
||||||
let mut blob_success = true;
|
let mut blob_success = true;
|
||||||
@@ -754,14 +825,19 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
|| Ok(()),
|
|| Ok(()),
|
||||||
))?
|
))?
|
||||||
.map(|avail: AvailabilityProcessingStatus| avail.try_into());
|
.map(|avail: AvailabilityProcessingStatus| avail.try_into());
|
||||||
let success = blob_success && result.as_ref().is_ok_and(|inner| inner.is_ok());
|
// Spec `on_block` is idempotent: re-importing an already-known block is a no-op
|
||||||
|
// success. Lighthouse surfaces this as `BlockError::DuplicateFullyImported`; the
|
||||||
|
// compliance suite re-feeds blocks repeatedly, so treat duplicates as success.
|
||||||
|
let is_duplicate = matches!(
|
||||||
|
&result,
|
||||||
|
Err(beacon_chain::BlockError::DuplicateFullyImported(_))
|
||||||
|
);
|
||||||
|
let success =
|
||||||
|
blob_success && (result.as_ref().is_ok_and(|inner| inner.is_ok()) || is_duplicate);
|
||||||
if success != valid {
|
if success != valid {
|
||||||
return Err(Error::DidntFail(format!(
|
return Err(Error::DidntFail(format!(
|
||||||
"block with root {} was valid={} whilst test expects valid={}. result: {:?}",
|
"block with root {} was valid={} whilst test expects valid={}. result: {:?}",
|
||||||
block_root,
|
block_root, success, valid, result
|
||||||
result.is_ok(),
|
|
||||||
valid,
|
|
||||||
result
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,19 +845,141 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
self.apply_invalid_block(&block)?;
|
self.apply_invalid_block(&block)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per spec test runner: an on_block step implies receiving block's attestations
|
Ok(())
|
||||||
// and attester slashings.
|
}
|
||||||
if success {
|
|
||||||
for attestation in block.message().body().attestations() {
|
fn block_attestations_have_divergent_indices(
|
||||||
let att = attestation.clone_as_attestation();
|
&self,
|
||||||
let _ = self.process_attestation(&att);
|
block: &SignedBeaconBlock<E>,
|
||||||
}
|
) -> Result<bool, Error> {
|
||||||
for attester_slashing in block.message().body().attester_slashings() {
|
let parent_root = block.parent_root();
|
||||||
self.process_attester_slashing(attester_slashing);
|
let Some(parent_block) = self
|
||||||
|
.harness
|
||||||
|
.chain
|
||||||
|
.get_blinded_block(&parent_root)
|
||||||
|
.map_err(|e| Error::InternalError(format!("failed to load parent block: {e:?}")))?
|
||||||
|
else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
let parent_state_root = parent_block.state_root();
|
||||||
|
let Some(mut block_context_state) = self
|
||||||
|
.harness
|
||||||
|
.chain
|
||||||
|
.get_state(
|
||||||
|
&parent_state_root,
|
||||||
|
Some(parent_block.slot()),
|
||||||
|
CACHE_STATE_IN_TESTS,
|
||||||
|
)
|
||||||
|
.map_err(|e| Error::InternalError(format!("failed to load parent state: {e:?}")))?
|
||||||
|
else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
complete_state_advance(
|
||||||
|
&mut block_context_state,
|
||||||
|
Some(parent_state_root),
|
||||||
|
block.slot(),
|
||||||
|
&self.harness.chain.spec,
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::InternalError(format!("failed to advance block context state: {e:?}"))
|
||||||
|
})?;
|
||||||
|
block_context_state
|
||||||
|
.build_all_committee_caches(&self.harness.chain.spec)
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::InternalError(format!(
|
||||||
|
"failed to build block context committee caches: {e:?}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for attestation in block.message().body().attestations() {
|
||||||
|
let attestation = attestation.clone_as_attestation();
|
||||||
|
let Ok(block_context_indexed) =
|
||||||
|
Self::indexed_attestation_from_state(&block_context_state, &attestation)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(target_context_indexed) =
|
||||||
|
self.indexed_attestation_from_target_state(&attestation)?
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if block_context_indexed.attesting_indices_to_vec()
|
||||||
|
!= target_context_indexed.attesting_indices_to_vec()
|
||||||
|
{
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indexed_attestation_from_target_state(
|
||||||
|
&self,
|
||||||
|
attestation: &Attestation<E>,
|
||||||
|
) -> Result<Option<IndexedAttestation<E>>, Error> {
|
||||||
|
let target_root = attestation.data().target.root;
|
||||||
|
let Some(target_block) = self
|
||||||
|
.harness
|
||||||
|
.chain
|
||||||
|
.canonical_head
|
||||||
|
.fork_choice_read_lock()
|
||||||
|
.get_block(&target_root)
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(mut target_state) = self
|
||||||
|
.harness
|
||||||
|
.chain
|
||||||
|
.store
|
||||||
|
.get_hot_state(&target_block.state_root, CACHE_STATE_IN_TESTS)
|
||||||
|
.map_err(|e| Error::InternalError(format!("failed to load target state: {e:?}")))?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let target_epoch_start_slot = attestation
|
||||||
|
.data()
|
||||||
|
.target
|
||||||
|
.epoch
|
||||||
|
.start_slot(E::slots_per_epoch());
|
||||||
|
complete_state_advance(
|
||||||
|
&mut target_state,
|
||||||
|
Some(target_block.state_root),
|
||||||
|
target_epoch_start_slot,
|
||||||
|
&self.harness.chain.spec,
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::InternalError(format!("failed to advance attestation target state: {e:?}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match Self::indexed_attestation_from_state(&target_state, attestation) {
|
||||||
|
Ok(indexed_attestation) => Ok(Some(indexed_attestation)),
|
||||||
|
Err(_) => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indexed_attestation_from_state(
|
||||||
|
state: &BeaconState<E>,
|
||||||
|
attestation: &Attestation<E>,
|
||||||
|
) -> Result<IndexedAttestation<E>, Error> {
|
||||||
|
match attestation.to_ref() {
|
||||||
|
AttestationRef::Base(att) => {
|
||||||
|
let committee = state
|
||||||
|
.get_beacon_committee(att.data.slot, att.data.index)
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::InternalError(format!("attestation committee lookup failed: {e:?}"))
|
||||||
|
})?;
|
||||||
|
attesting_indices_base::get_indexed_attestation(committee.committee, att).map_err(
|
||||||
|
|e| Error::InternalError(format!("attestation indexing failed: {e:?}")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AttestationRef::Electra(att) => {
|
||||||
|
attesting_indices_electra::get_indexed_attestation_from_state(state, att).map_err(
|
||||||
|
|e| Error::InternalError(format!("attestation indexing failed: {e:?}")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply invalid blocks directly against the fork choice `on_block` function. This ensures
|
// Apply invalid blocks directly against the fork choice `on_block` function. This ensures
|
||||||
@@ -824,6 +1022,9 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
.seconds_from_current_slot_start()
|
.seconds_from_current_slot_start()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// FIXME(sproul): this whole concept is a bit ill-conceived, the blocks just get
|
||||||
|
// rejected here due to passing payload status irrelevant, which is not a real codepath
|
||||||
|
// that should be reached
|
||||||
let result = self
|
let result = self
|
||||||
.harness
|
.harness
|
||||||
.chain
|
.chain
|
||||||
@@ -852,11 +1053,14 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_attestation(&self, attestation: &Attestation<E>) -> Result<(), Error> {
|
pub fn process_attestation(&self, attestation: &Attestation<E>) -> Result<(), Error> {
|
||||||
let (indexed_attestation, _) = obtain_indexed_attestation_and_committees_per_slot(
|
let indexed_attestation = self
|
||||||
&self.harness.chain,
|
.indexed_attestation_from_target_state(attestation)?
|
||||||
attestation.to_ref(),
|
.ok_or_else(|| {
|
||||||
)
|
Error::InternalError(format!(
|
||||||
.map_err(|e| Error::InternalError(format!("attestation indexing failed with {:?}", e)))?;
|
"attestation target block {:?} unknown or could not be indexed from target state",
|
||||||
|
attestation.data().target.root
|
||||||
|
))
|
||||||
|
})?;
|
||||||
let verified_attestation: ManuallyVerifiedAttestation<EphemeralHarnessType<E>> =
|
let verified_attestation: ManuallyVerifiedAttestation<EphemeralHarnessType<E>> =
|
||||||
ManuallyVerifiedAttestation {
|
ManuallyVerifiedAttestation {
|
||||||
attestation,
|
attestation,
|
||||||
@@ -869,12 +1073,44 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
.map_err(|e| Error::InternalError(format!("attestation import failed with {:?}", e)))
|
.map_err(|e| Error::InternalError(format!("attestation import failed with {:?}", e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_attester_slashing(&self, attester_slashing: AttesterSlashingRef<E>) {
|
pub fn process_attester_slashing(
|
||||||
|
&self,
|
||||||
|
attester_slashing: AttesterSlashingRef<E>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let justified_block = {
|
||||||
|
let fork_choice = self.harness.chain.canonical_head.fork_choice_read_lock();
|
||||||
|
fork_choice
|
||||||
|
.get_block(&fork_choice.justified_checkpoint().root)
|
||||||
|
.ok_or_else(|| Error::InternalError("justified block not found".into()))?
|
||||||
|
};
|
||||||
|
let justified_state = self
|
||||||
|
.harness
|
||||||
|
.chain
|
||||||
|
.store
|
||||||
|
.get_hot_state(&justified_block.state_root, CACHE_STATE_IN_TESTS)
|
||||||
|
.map_err(|e| Error::InternalError(format!("failed to load justified state: {e:?}")))?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::InternalError(format!(
|
||||||
|
"justified state {:?} not found",
|
||||||
|
justified_block.state_root
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
verify_attester_slashing(
|
||||||
|
&justified_state,
|
||||||
|
attester_slashing,
|
||||||
|
VerifySignatures::True,
|
||||||
|
&self.harness.chain.spec,
|
||||||
|
)
|
||||||
|
.map_err(|e| Error::InternalError(format!("invalid attester slashing: {e:?}")))?;
|
||||||
|
|
||||||
self.harness
|
self.harness
|
||||||
.chain
|
.chain
|
||||||
.canonical_head
|
.canonical_head
|
||||||
.fork_choice_write_lock()
|
.fork_choice_write_lock()
|
||||||
.on_attester_slashing(attester_slashing)
|
.on_attester_slashing(attester_slashing);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_pow_block(&self, pow_block: &PowBlock) {
|
pub fn process_pow_block(&self, pow_block: &PowBlock) {
|
||||||
@@ -1148,6 +1384,61 @@ impl<E: EthSpec> Tester<E> {
|
|||||||
check_equal("head_payload_status", actual, expected_status)
|
check_equal("head_payload_status", actual, expected_status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn check_viable_for_head_roots_and_weights(
|
||||||
|
&self,
|
||||||
|
expected: &[RootAndWeight],
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// Apply pending vote deltas so weights reflect the latest store state.
|
||||||
|
let _ = self.find_head()?;
|
||||||
|
|
||||||
|
let fork_choice = self.harness.chain.canonical_head.fork_choice_read_lock();
|
||||||
|
let justified = fork_choice.justified_checkpoint();
|
||||||
|
let finalized = fork_choice.finalized_checkpoint();
|
||||||
|
let current_slot = fork_choice.fc_store().get_current_slot();
|
||||||
|
let proposer_boost_root = fork_choice.proposer_boost_root();
|
||||||
|
let justified_balances = fork_choice.fc_store().justified_balances().clone();
|
||||||
|
let actual = fork_choice
|
||||||
|
.proto_array()
|
||||||
|
.filtered_block_tree_leaves_and_weights::<E>(
|
||||||
|
&justified.root,
|
||||||
|
current_slot,
|
||||||
|
justified,
|
||||||
|
finalized,
|
||||||
|
proposer_boost_root,
|
||||||
|
&justified_balances,
|
||||||
|
&self.spec,
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::InternalError(format!(
|
||||||
|
"filtered_block_tree_leaves_and_weights failed: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
drop(fork_choice);
|
||||||
|
|
||||||
|
let mut actual_sorted: Vec<(Hash256, u8, u64)> = actual
|
||||||
|
.into_iter()
|
||||||
|
.map(|(root, status, weight)| (root, status as u8, weight))
|
||||||
|
.collect();
|
||||||
|
actual_sorted.sort();
|
||||||
|
let mut expected_sorted: Vec<(Hash256, u8, u64)> = expected
|
||||||
|
.iter()
|
||||||
|
.map(|x| {
|
||||||
|
(
|
||||||
|
x.root,
|
||||||
|
x.payload_status.unwrap_or(FcPayloadStatus::Pending as u8),
|
||||||
|
x.weight,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
expected_sorted.sort();
|
||||||
|
|
||||||
|
check_equal(
|
||||||
|
"viable_for_head_roots_and_weights",
|
||||||
|
actual_sorted,
|
||||||
|
expected_sorted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_should_override_fcu(
|
pub fn check_should_override_fcu(
|
||||||
&self,
|
&self,
|
||||||
expected_should_override_fcu: ShouldOverrideFcu,
|
expected_should_override_fcu: ShouldOverrideFcu,
|
||||||
|
|||||||
@@ -745,6 +745,48 @@ impl<E: EthSpec + TypeName> Handler for ForkChoiceHandler<E> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ForkChoiceComplianceHandler<E> {
|
||||||
|
handler_name: String,
|
||||||
|
_phantom: PhantomData<E>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: EthSpec> ForkChoiceComplianceHandler<E> {
|
||||||
|
pub fn new(handler_name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
handler_name: handler_name.into(),
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: EthSpec + TypeName> Handler for ForkChoiceComplianceHandler<E> {
|
||||||
|
type Case = cases::ForkChoiceTest<E>;
|
||||||
|
|
||||||
|
fn config_name() -> &'static str {
|
||||||
|
E::name()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runner_name() -> &'static str {
|
||||||
|
"fork_choice_compliance"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handler_name(&self) -> String {
|
||||||
|
self.handler_name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_rayon() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool {
|
||||||
|
cfg!(feature = "fake_crypto") && fork_name.fulu_enabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disabled_forks(&self) -> Vec<ForkName> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Educe)]
|
#[derive(Educe)]
|
||||||
#[educe(Default)]
|
#[educe(Default)]
|
||||||
pub struct OptimisticSyncHandler<E>(PhantomData<E>);
|
pub struct OptimisticSyncHandler<E>(PhantomData<E>);
|
||||||
|
|||||||
@@ -1085,6 +1085,37 @@ fn fork_choice_on_payload_attestation_message() {
|
|||||||
ForkChoiceHandler::<MainnetEthSpec>::new("on_payload_attestation_message").run();
|
ForkChoiceHandler::<MainnetEthSpec>::new("on_payload_attestation_message").run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fork_choice_compliance_attester_slashing_test() {
|
||||||
|
ForkChoiceComplianceHandler::<MinimalEthSpec>::new("attester_slashing_test").run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fork_choice_compliance_block_cover_test() {
|
||||||
|
ForkChoiceComplianceHandler::<MinimalEthSpec>::new("block_cover_test").run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fork_choice_compliance_block_tree_test() {
|
||||||
|
ForkChoiceComplianceHandler::<MinimalEthSpec>::new("block_tree_test").run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fork_choice_compliance_block_weight_test() {
|
||||||
|
ForkChoiceComplianceHandler::<MinimalEthSpec>::new("block_weight_test").run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn fork_choice_compliance_invalid_message_test() {
|
||||||
|
ForkChoiceComplianceHandler::<MinimalEthSpec>::new("invalid_message_test").run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fork_choice_compliance_shuffling_test() {
|
||||||
|
ForkChoiceComplianceHandler::<MinimalEthSpec>::new("shuffling_test").run();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn optimistic_sync() {
|
fn optimistic_sync() {
|
||||||
OptimisticSyncHandler::<MinimalEthSpec>::default().run();
|
OptimisticSyncHandler::<MinimalEthSpec>::default().run();
|
||||||
|
|||||||
Reference in New Issue
Block a user