diff --git a/tests/consensus/lstar/fork_choice/test_signature_aggregation.py b/tests/consensus/lstar/fork_choice/test_signature_aggregation.py index 16c49889a..7de3d1158 100644 --- a/tests/consensus/lstar/fork_choice/test_signature_aggregation.py +++ b/tests/consensus/lstar/fork_choice/test_signature_aggregation.py @@ -8,19 +8,22 @@ BlockSpec, BlockStep, ForkChoiceTestFiller, + GossipAggregatedAttestationStep, StoreChecks, + TickStep, ) -from lean_spec.spec.forks import Slot, ValidatorIndex +from lean_spec.spec.forks import Interval, Slot, ValidatorIndex pytestmark = pytest.mark.valid_until("Lstar") @pytest.mark.real_crypto(smoke=True) -def test_multiple_specs_same_target_merge_into_one( +def test_multiple_attestations_same_target_merge_into_one( fork_choice_test: ForkChoiceTestFiller, ) -> None: """ - Two attestations sharing one target merge into a single aggregation. + Two attestations in the known pool sharing one target get merged + into a single aggregation. Given ----- @@ -83,6 +86,90 @@ def test_multiple_specs_same_target_merge_into_one( ) +@pytest.mark.real_crypto(smoke=True) +def test_overlapping_proofs_same_target_recursively_merge_into_one( + fork_choice_test: ForkChoiceTestFiller, +) -> None: + """ + Two overlapping proofs for one target fold into a single in-block attestation. + + Given + ----- + - 4 validators. + - the chain: + genesis -> block_1(1) + - one proof covers V0, V1, V2 targeting block_1. + - one proof covers V1, V2, V3 targeting block_1. + - the two proofs overlap on V1, V2. + - both proofs carry identical attestation data. + - both proofs wait unmerged in the known pool. + + When + ---- + - block_2 is built on block_1, carrying no votes of its own. + + Then + ---- + - block_2 holds 1 aggregated attestation. + - that aggregation covers V0, V1, V2, V3. + - head is block_2. + - head is at slot 2. + + Timing + ------ + - the proofs are gossipped at slot 1, interval 3. + - interval 3 is past the aggregate phase. + """ + fork_choice_test( + steps=[ + BlockStep( + block=BlockSpec(slot=Slot(1), label="block_1"), + checks=StoreChecks(head_slot=Slot(1)), + ), + TickStep(interval=int(Interval.from_slot(Slot(1))) + 3), + GossipAggregatedAttestationStep( + attestation=AggregatedAttestationSpec( + validator_indices=[ + ValidatorIndex(0), + ValidatorIndex(1), + ValidatorIndex(2), + ], + slot=Slot(1), + target_slot=Slot(1), + target_root_label="block_1", + ), + ), + GossipAggregatedAttestationStep( + attestation=AggregatedAttestationSpec( + validator_indices=[ + ValidatorIndex(1), + ValidatorIndex(2), + ValidatorIndex(3), + ], + slot=Slot(1), + target_slot=Slot(1), + target_root_label="block_1", + ), + ), + BlockStep( + block=BlockSpec(slot=Slot(2), label="block_2"), + checks=StoreChecks( + head_slot=Slot(2), + head_root_label="block_2", + block_attestation_count=1, + block_attestations=[ + AggregatedAttestationCheck( + participants={0, 1, 2, 3}, + attestation_slot=Slot(1), + target_slot=Slot(1), + ), + ], + ), + ), + ], + ) + + def test_different_targets_create_separate_aggregations( fork_choice_test: ForkChoiceTestFiller, ) -> None: