From a9f43f9934be3b59d652419ec809466e99060168 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:19:57 -0500 Subject: [PATCH] Add mixed V17/V29 execution payload invalidation test (#9089) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- .../gloas_payload.rs | 76 +++++++++++++++++++ consensus/proto_array/src/proto_array.rs | 1 - 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs b/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs index ea37780795..2e792028e5 100644 --- a/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs +++ b/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs @@ -890,4 +890,80 @@ mod tests { let test = get_gloas_payload_received_interleaving_test_definition(); test.run(); } + + /// Test that execution payload invalidation propagates across the V17→V29 fork + /// boundary: after invalidating a V17 parent, head must not select any descendant. + /// + /// genesis(V17) -> block_1(V17, slot 31) -> block_2(V29, slot 32) + #[test] + fn mixed_v17_v29_invalidation() { + let balances = vec![1]; + let mut ops = vec![]; + + // V17 block at slot 31 (pre-Gloas). + ops.push(Operation::ProcessBlock { + slot: Slot::new(31), + root: get_root(1), + parent_root: get_root(0), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + execution_payload_parent_hash: None, + execution_payload_block_hash: None, + }); + + // V29 block at slot 32 (first Gloas slot), child of block 1. + ops.push(Operation::ProcessBlock { + slot: Slot::new(32), + root: get_root(2), + parent_root: get_root(1), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + execution_payload_parent_hash: Some(get_hash(1)), + execution_payload_block_hash: Some(get_hash(2)), + }); + + // Vote for block 2 (V29) so both blocks have weight. + ops.push(Operation::ProcessAttestation { + validator_index: 0, + block_root: get_root(2), + attestation_slot: Slot::new(32), + }); + + // FindHead triggers apply_score_changes which materializes the vote. + ops.push(Operation::FindHead { + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + justified_state_balances: balances.clone(), + expected_head: get_root(2), + current_slot: Slot::new(32), + expected_payload_status: None, + }); + + // Invalidate block 1 (V17). filter_block_tree excludes the entire branch. + ops.push(Operation::InvalidatePayload { + head_block_root: get_root(1), + latest_valid_ancestor_root: Some(get_hash(0)), + }); + + // Head falls back to genesis — the invalid branch is no longer selectable. + ops.push(Operation::FindHead { + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + justified_state_balances: balances.clone(), + expected_head: get_root(0), + current_slot: Slot::new(32), + expected_payload_status: None, + }); + + ForkChoiceTestDefinition { + finalized_block_slot: Slot::new(0), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + operations: ops, + execution_payload_parent_hash: None, + execution_payload_block_hash: None, + spec: Some(gloas_fork_boundary_spec()), + } + .run(); + } } diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 1f7291b260..4946631f73 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -864,7 +864,6 @@ impl ProtoArray { /// Invalidate zero or more blocks, as specified by the `InvalidationOperation`. /// /// See the documentation of `InvalidationOperation` for usage. - // TODO(gloas): this needs some tests for the mixed Gloas/pre-Gloas case. pub fn propagate_execution_payload_invalidation( &mut self, op: &InvalidationOperation,