mod execution_status; mod ffg_updates; mod gloas_payload; mod no_votes; mod votes; use crate::proto_array_fork_choice::{Block, ExecutionStatus, PayloadStatus, ProtoArrayForkChoice}; use crate::{InvalidationOperation, JustifiedBalances}; use fixed_bytes::FixedBytesExtended; use serde::{Deserialize, Serialize}; use ssz::BitVector; use std::collections::BTreeSet; use std::time::Duration; use types::{ AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, Slot, }; pub use execution_status::*; pub use ffg_updates::*; pub use gloas_payload::*; pub use no_votes::*; pub use votes::*; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Operation { FindHead { justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, justified_state_balances: Vec, expected_head: Hash256, current_slot: Slot, #[serde(default)] expected_payload_status: Option, }, ProposerBoostFindHead { justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, justified_state_balances: Vec, expected_head: Hash256, proposer_boost_root: Hash256, }, InvalidFindHead { justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, justified_state_balances: Vec, }, ProcessBlock { slot: Slot, root: Hash256, parent_root: Hash256, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, #[serde(default)] execution_payload_parent_hash: Option, #[serde(default)] execution_payload_block_hash: Option, }, ProcessAttestation { validator_index: usize, block_root: Hash256, attestation_slot: Slot, }, ProcessPayloadAttestation { validator_index: usize, block_root: Hash256, attestation_slot: Slot, payload_present: bool, #[serde(default)] blob_data_available: bool, }, Prune { finalized_root: Hash256, prune_threshold: usize, expected_len: usize, }, InvalidatePayload { head_block_root: Hash256, latest_valid_ancestor_root: Option, }, AssertWeight { block_root: Hash256, weight: u64, }, AssertPayloadWeights { block_root: Hash256, expected_full_weight: u64, expected_empty_weight: u64, }, AssertParentPayloadStatus { block_root: Hash256, expected_status: PayloadStatus, }, SetPayloadTiebreak { block_root: Hash256, is_timely: bool, is_data_available: bool, }, /// Simulate receiving and validating an execution payload for `block_root`. /// Sets `payload_received = true` on the V29 node via the live validation path. ProcessExecutionPayloadEnvelope { block_root: Hash256, }, AssertPayloadReceived { block_root: Hash256, expected: bool, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ForkChoiceTestDefinition { pub finalized_block_slot: Slot, pub justified_checkpoint: Checkpoint, pub finalized_checkpoint: Checkpoint, pub operations: Vec, #[serde(default)] pub execution_payload_parent_hash: Option, #[serde(default)] pub execution_payload_block_hash: Option, #[serde(skip)] pub spec: Option, } impl ForkChoiceTestDefinition { pub fn run(self) { let spec = self.spec.unwrap_or_else(|| { let mut spec = MainnetEthSpec::default_spec(); spec.proposer_score_boost = Some(50); // Legacy test definitions target pre-Gloas behaviour unless explicitly overridden. spec.gloas_fork_epoch = None; spec }); let junk_shuffling_id = AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero()); let mut fork_choice = ProtoArrayForkChoice::new::( self.finalized_block_slot, self.finalized_block_slot, Hash256::zero(), self.justified_checkpoint, self.finalized_checkpoint, junk_shuffling_id.clone(), junk_shuffling_id, ExecutionStatus::Optimistic(ExecutionBlockHash::zero()), self.execution_payload_parent_hash, self.execution_payload_block_hash, 0, &spec, ) .expect("should create fork choice struct"); let equivocating_indices = BTreeSet::new(); for (op_index, op) in self.operations.into_iter().enumerate() { match op.clone() { Operation::FindHead { justified_checkpoint, finalized_checkpoint, justified_state_balances, expected_head, current_slot, expected_payload_status, } => { let justified_balances = JustifiedBalances::from_effective_balances(justified_state_balances) .unwrap(); let (head, payload_status) = fork_choice .find_head::( justified_checkpoint, finalized_checkpoint, &justified_balances, Hash256::zero(), &equivocating_indices, current_slot, &spec, ) .unwrap_or_else(|e| { panic!("find_head op at index {} returned error {}", op_index, e) }); assert_eq!( head, expected_head, "Operation at index {} failed head check. Operation: {:?}", op_index, op ); if let Some(expected_status) = expected_payload_status { assert_eq!( payload_status, expected_status, "Operation at index {} failed payload status check. Operation: {:?}", op_index, op ); } check_bytes_round_trip(&fork_choice); } Operation::ProposerBoostFindHead { justified_checkpoint, finalized_checkpoint, justified_state_balances, expected_head, proposer_boost_root, } => { let justified_balances = JustifiedBalances::from_effective_balances(justified_state_balances) .unwrap(); let (head, _payload_status) = fork_choice .find_head::( justified_checkpoint, finalized_checkpoint, &justified_balances, proposer_boost_root, &equivocating_indices, Slot::new(0), &spec, ) .unwrap_or_else(|e| { panic!("find_head op at index {} returned error {}", op_index, e) }); assert_eq!( head, expected_head, "Operation at index {} failed head check. Operation: {:?}", op_index, op ); check_bytes_round_trip(&fork_choice); } Operation::InvalidFindHead { justified_checkpoint, finalized_checkpoint, justified_state_balances, } => { let justified_balances = JustifiedBalances::from_effective_balances(justified_state_balances) .unwrap(); let result = fork_choice.find_head::( justified_checkpoint, finalized_checkpoint, &justified_balances, Hash256::zero(), &equivocating_indices, Slot::new(0), &spec, ); assert!( result.is_err(), "Operation at index {} . Operation: {:?}", op_index, op ); check_bytes_round_trip(&fork_choice); } Operation::ProcessBlock { slot, root, parent_root, justified_checkpoint, finalized_checkpoint, execution_payload_parent_hash, execution_payload_block_hash, } => { let block = Block { slot, root, parent_root: Some(parent_root), state_root: Hash256::zero(), target_root: Hash256::zero(), current_epoch_shuffling_id: AttestationShufflingId::from_components( Epoch::new(0), Hash256::zero(), ), next_epoch_shuffling_id: AttestationShufflingId::from_components( Epoch::new(0), Hash256::zero(), ), justified_checkpoint, finalized_checkpoint, // All blocks are imported optimistically. execution_status: ExecutionStatus::Optimistic( ExecutionBlockHash::from_root(root), ), unrealized_justified_checkpoint: None, unrealized_finalized_checkpoint: None, execution_payload_parent_hash, execution_payload_block_hash, proposer_index: Some(0), }; fork_choice .process_block::(block, slot, &spec, Duration::ZERO) .unwrap_or_else(|e| { panic!( "process_block op at index {} returned error: {:?}", op_index, e ) }); check_bytes_round_trip(&fork_choice); } Operation::ProcessAttestation { validator_index, block_root, attestation_slot, } => { fork_choice .process_attestation(validator_index, block_root, attestation_slot, false) .unwrap_or_else(|_| { panic!( "process_attestation op at index {} returned error", op_index ) }); check_bytes_round_trip(&fork_choice); } Operation::ProcessPayloadAttestation { validator_index, block_root, attestation_slot: _, payload_present, blob_data_available, } => { fork_choice .process_payload_attestation( block_root, validator_index, payload_present, blob_data_available, ) .unwrap_or_else(|_| { panic!( "process_payload_attestation op at index {} returned error", op_index ) }); check_bytes_round_trip(&fork_choice); } Operation::Prune { finalized_root, prune_threshold, expected_len, } => { fork_choice.set_prune_threshold(prune_threshold); fork_choice .maybe_prune(finalized_root) .expect("update_finalized_root op at index {} returned error"); // Ensure that no pruning happened. assert_eq!( fork_choice.len(), expected_len, "Prune op at index {} failed with {} instead of {}", op_index, fork_choice.len(), expected_len ); } Operation::InvalidatePayload { head_block_root, latest_valid_ancestor_root, } => { let op = if let Some(latest_valid_ancestor) = latest_valid_ancestor_root { InvalidationOperation::InvalidateMany { head_block_root, always_invalidate_head: true, latest_valid_ancestor, } } else { InvalidationOperation::InvalidateOne { block_root: head_block_root, } }; fork_choice .process_execution_payload_invalidation::( &op, self.finalized_checkpoint, ) .unwrap() } Operation::AssertWeight { block_root, weight } => assert_eq!( fork_choice.get_weight(&block_root).unwrap(), weight, "block weight at op index {}", op_index ), Operation::AssertPayloadWeights { block_root, expected_full_weight, expected_empty_weight, } => { let block_index = fork_choice .proto_array .indices .get(&block_root) .unwrap_or_else(|| { panic!( "AssertPayloadWeights: block root not found at op index {}", op_index ) }); let node = fork_choice .proto_array .nodes .get(*block_index) .unwrap_or_else(|| { panic!( "AssertPayloadWeights: node not found at op index {}", op_index ) }); let v29 = node.as_v29().unwrap_or_else(|_| { panic!( "AssertPayloadWeights: node is not V29 at op index {}", op_index ) }); assert_eq!( v29.full_payload_weight, expected_full_weight, "full_payload_weight mismatch at op index {}", op_index ); assert_eq!( v29.empty_payload_weight, expected_empty_weight, "empty_payload_weight mismatch at op index {}", op_index ); } Operation::AssertParentPayloadStatus { block_root, expected_status, } => { let block_index = fork_choice .proto_array .indices .get(&block_root) .unwrap_or_else(|| { panic!( "AssertParentPayloadStatus: block root not found at op index {}", op_index ) }); let node = fork_choice .proto_array .nodes .get(*block_index) .unwrap_or_else(|| { panic!( "AssertParentPayloadStatus: node not found at op index {}", op_index ) }); let v29 = node.as_v29().unwrap_or_else(|_| { panic!( "AssertParentPayloadStatus: node is not V29 at op index {}", op_index ) }); assert_eq!( v29.parent_payload_status, expected_status, "parent_payload_status mismatch at op index {}", op_index ); } Operation::SetPayloadTiebreak { block_root, is_timely, is_data_available, } => { let block_index = fork_choice .proto_array .indices .get(&block_root) .unwrap_or_else(|| { panic!( "SetPayloadTiebreak: block root not found at op index {}", op_index ) }); let node = fork_choice .proto_array .nodes .get_mut(*block_index) .unwrap_or_else(|| { panic!( "SetPayloadTiebreak: node not found at op index {}", op_index ) }); let node_v29 = node.as_v29_mut().unwrap_or_else(|_| { panic!( "SetPayloadTiebreak: node is not V29 at op index {}", op_index ) }); // Set all bits (exceeds any threshold) or clear all bits. let fill = if is_timely { 0xFF } else { 0x00 }; node_v29.payload_timeliness_votes = BitVector::from_bytes(smallvec::smallvec![fill; 64]) .expect("valid 512-bit bitvector"); let fill = if is_data_available { 0xFF } else { 0x00 }; node_v29.payload_data_availability_votes = BitVector::from_bytes(smallvec::smallvec![fill; 64]) .expect("valid 512-bit bitvector"); // Per spec, is_payload_timely/is_payload_data_available require // the payload to be in payload_states (payload_received). node_v29.payload_received = is_timely || is_data_available; } Operation::ProcessExecutionPayloadEnvelope { block_root } => { fork_choice .on_valid_payload_envelope_received(block_root) .unwrap_or_else(|e| { panic!( "on_execution_payload op at index {} returned error: {}", op_index, e ) }); check_bytes_round_trip(&fork_choice); } Operation::AssertPayloadReceived { block_root, expected, } => { let actual = fork_choice.is_payload_received(&block_root); assert_eq!( actual, expected, "payload_received mismatch at op index {}", op_index ); } } } } } /// Gives a root that is not the zero hash (unless i is `usize::MAX)`. fn get_root(i: u64) -> Hash256 { Hash256::from_low_u64_be(i + 1) } /// Gives a hash that is not the zero hash (unless i is `usize::MAX)`. fn get_hash(i: u64) -> ExecutionBlockHash { ExecutionBlockHash::from_root(get_root(i)) } /// Gives a checkpoint with a root that is not the zero hash (unless i is `usize::MAX)`. /// `Epoch` will always equal `i`. fn get_checkpoint(i: u64) -> Checkpoint { Checkpoint { epoch: Epoch::new(i), root: get_root(i), } } fn check_bytes_round_trip(original: &ProtoArrayForkChoice) { let bytes = original.as_bytes(); let decoded = ProtoArrayForkChoice::from_bytes(&bytes, original.balances.clone()) .expect("fork choice should decode from bytes"); assert!( *original == decoded, "fork choice should encode and decode without change" ); }