Skip to content

Commit 1937162

Browse files
unnawuttcoratgerclaude
authored
test: add test vector for block production with merging aggregates (leanEthereum#897)
* test(consensus): cover in-block recursive merge of overlapping pool proofs The block builder's collapse-by-data branch in build_block merges multiple single-message aggregates for the same AttestationData via SingleMessageAggregate.aggregate(children=..., raw_xmss=[]). This is the only proposal-time path that constructs a recursive single-message aggregate, and no existing fork-choice vector reached it because the local aggregator absorbs same-data proofs before block build time. Gossiping two aggregated proofs with overlapping participant sets at slot 2 interval 3 (past the aggregate phase) bypasses the local aggregator, so both proofs migrate side by side into the known pool at slot 2 interval 4. The greedy picker takes both because the second still adds one uncovered validator on top of the first, exercising the merge branch on overlapping children rather than the unrealistic fully disjoint shape. * address comments * fix lint * test(consensus): tighten head assertion and docstring for recursive-fold vector Assert head identity (head_root_label) alongside head_slot so the Then "head is block_2" claim is verified by root, not just slot. Docstring: split the phase-glossing parenthetical and the two-fact head bullet into one-fact-per-line bullets, and drop the Given gossip bullet that duplicated the Timing section, per the consensus test-vector documentation standard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e751986 commit 1937162

1 file changed

Lines changed: 90 additions & 3 deletions

File tree

tests/consensus/lstar/fork_choice/test_signature_aggregation.py

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,22 @@
88
BlockSpec,
99
BlockStep,
1010
ForkChoiceTestFiller,
11+
GossipAggregatedAttestationStep,
1112
StoreChecks,
13+
TickStep,
1214
)
13-
from lean_spec.spec.forks import Slot, ValidatorIndex
15+
from lean_spec.spec.forks import Interval, Slot, ValidatorIndex
1416

1517
pytestmark = pytest.mark.valid_until("Lstar")
1618

1719

1820
@pytest.mark.real_crypto(smoke=True)
19-
def test_multiple_specs_same_target_merge_into_one(
21+
def test_multiple_attestations_same_target_merge_into_one(
2022
fork_choice_test: ForkChoiceTestFiller,
2123
) -> None:
2224
"""
23-
Two attestations sharing one target merge into a single aggregation.
25+
Two attestations in the known pool sharing one target get merged
26+
into a single aggregation.
2427
2528
Given
2629
-----
@@ -83,6 +86,90 @@ def test_multiple_specs_same_target_merge_into_one(
8386
)
8487

8588

89+
@pytest.mark.real_crypto(smoke=True)
90+
def test_overlapping_proofs_same_target_recursively_merge_into_one(
91+
fork_choice_test: ForkChoiceTestFiller,
92+
) -> None:
93+
"""
94+
Two overlapping proofs for one target fold into a single in-block attestation.
95+
96+
Given
97+
-----
98+
- 4 validators.
99+
- the chain:
100+
genesis -> block_1(1)
101+
- one proof covers V0, V1, V2 targeting block_1.
102+
- one proof covers V1, V2, V3 targeting block_1.
103+
- the two proofs overlap on V1, V2.
104+
- both proofs carry identical attestation data.
105+
- both proofs wait unmerged in the known pool.
106+
107+
When
108+
----
109+
- block_2 is built on block_1, carrying no votes of its own.
110+
111+
Then
112+
----
113+
- block_2 holds 1 aggregated attestation.
114+
- that aggregation covers V0, V1, V2, V3.
115+
- head is block_2.
116+
- head is at slot 2.
117+
118+
Timing
119+
------
120+
- the proofs are gossipped at slot 1, interval 3.
121+
- interval 3 is past the aggregate phase.
122+
"""
123+
fork_choice_test(
124+
steps=[
125+
BlockStep(
126+
block=BlockSpec(slot=Slot(1), label="block_1"),
127+
checks=StoreChecks(head_slot=Slot(1)),
128+
),
129+
TickStep(interval=int(Interval.from_slot(Slot(1))) + 3),
130+
GossipAggregatedAttestationStep(
131+
attestation=AggregatedAttestationSpec(
132+
validator_indices=[
133+
ValidatorIndex(0),
134+
ValidatorIndex(1),
135+
ValidatorIndex(2),
136+
],
137+
slot=Slot(1),
138+
target_slot=Slot(1),
139+
target_root_label="block_1",
140+
),
141+
),
142+
GossipAggregatedAttestationStep(
143+
attestation=AggregatedAttestationSpec(
144+
validator_indices=[
145+
ValidatorIndex(1),
146+
ValidatorIndex(2),
147+
ValidatorIndex(3),
148+
],
149+
slot=Slot(1),
150+
target_slot=Slot(1),
151+
target_root_label="block_1",
152+
),
153+
),
154+
BlockStep(
155+
block=BlockSpec(slot=Slot(2), label="block_2"),
156+
checks=StoreChecks(
157+
head_slot=Slot(2),
158+
head_root_label="block_2",
159+
block_attestation_count=1,
160+
block_attestations=[
161+
AggregatedAttestationCheck(
162+
participants={0, 1, 2, 3},
163+
attestation_slot=Slot(1),
164+
target_slot=Slot(1),
165+
),
166+
],
167+
),
168+
),
169+
],
170+
)
171+
172+
86173
def test_different_targets_create_separate_aggregations(
87174
fork_choice_test: ForkChoiceTestFiller,
88175
) -> None:

0 commit comments

Comments
 (0)