Skip to content

Commit 003968f

Browse files
tcoratgerclaude
andauthored
test: vector for fallback-pool set-cover contribution (leanEthereum#1024)
Seed the accepted pool and the pending pool with partially overlapping subsets for one attestation data, then run the aggregation round so the fallback pool must contribute the still-uncovered validator. A new pending-pool participant-union store check pins that the merged proof covers the union, isolating the fallback path from single-pool block production. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9db7d66 commit 003968f

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

packages/testing/src/consensus_testing/test_types/store_checks.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,15 @@ class StoreChecks(SelectiveCheck):
246246
aggregated proof map.
247247
"""
248248

249+
new_pool_proof_participants: dict[Slot, set[int]] | None = None
250+
"""
251+
Expected union of participants across pending-pool proofs, keyed by target slot.
252+
253+
Compares the set of validator indices covered by every proof in the
254+
pending aggregated proof map for each target slot.
255+
Pins the coverage a fresh aggregation round produced in the pending pool.
256+
"""
257+
249258
block_attestation_count: int | None = None
250259
"""
251260
Expected number of aggregated attestations in the block body.
@@ -428,6 +437,22 @@ def _resolve(label: str) -> Bytes32:
428437
expected_target_slots = sorted(getattr(self, field_name))
429438
_check(field_name, actual_target_slots, expected_target_slots)
430439

440+
# Participant union across pending-pool proofs, per target slot
441+
if "new_pool_proof_participants" in fields:
442+
assert self.new_pool_proof_participants is not None
443+
participants_by_target_slot: dict[Slot, set[int]] = {}
444+
for attestation_data, proofs in store.latest_new_aggregated_payloads.items():
445+
target_slot = attestation_data.target.slot
446+
covered = participants_by_target_slot.setdefault(target_slot, set())
447+
for proof in proofs:
448+
covered |= {int(i) for i in proof.participants.to_validator_indices()}
449+
for target_slot, expected_participants in self.new_pool_proof_participants.items():
450+
_check(
451+
f"new_pool_proof_participants[{target_slot}]",
452+
participants_by_target_slot.get(target_slot, set()),
453+
expected_participants,
454+
)
455+
431456
# Block body attestation count
432457
if "block_attestation_count" in fields:
433458
if filled_block is None:
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Greedy set-cover draws from the fallback pool after the priority pool."""
2+
3+
import pytest
4+
5+
from consensus_testing import (
6+
AggregatedAttestationCheck,
7+
AggregatedAttestationSpec,
8+
BlockSpec,
9+
BlockStep,
10+
ForkChoiceTestFiller,
11+
GossipAggregatedAttestationStep,
12+
StoreChecks,
13+
TickStep,
14+
)
15+
from lean_spec.spec.forks import Interval, Slot, ValidatorIndex
16+
17+
pytestmark = pytest.mark.valid_until("Lstar")
18+
19+
20+
def test_aggregate_covers_union_of_priority_and_fallback_pools(
21+
fork_choice_test: ForkChoiceTestFiller,
22+
) -> None:
23+
"""
24+
Aggregation merges a fallback-pool proof with a priority-pool proof for one target.
25+
26+
Given
27+
-----
28+
- 4 validators.
29+
- the chain:
30+
genesis -> block_1(1)
31+
- one proof covers V0, V1 targeting block_1.
32+
- an acceptance tick migrates that proof into the accepted pool.
33+
- one proof covers V1, V2 targeting block_1.
34+
- that proof stays in the pending pool.
35+
- the two proofs share V1 and carry identical attestation data.
36+
37+
When
38+
----
39+
- the aggregation interval runs with both pools populated.
40+
- an acceptance tick migrates the merged proof into the accepted pool.
41+
- block_3 is built on block_1, carrying no votes of its own.
42+
43+
Then
44+
----
45+
- the pending (priority) proof is taken before the accepted (fallback) proof.
46+
- the fallback proof adds V0, the one validator still uncovered.
47+
- the pending pool holds one proof covering V0, V1, V2 for target slot 1.
48+
- block_3 holds 1 aggregated attestation.
49+
- that aggregation covers V0, V1, V2.
50+
- head is block_3.
51+
"""
52+
fork_choice_test(
53+
steps=[
54+
BlockStep(
55+
block=BlockSpec(slot=Slot(1), label="block_1"),
56+
checks=StoreChecks(head_slot=Slot(1)),
57+
),
58+
TickStep(interval=int(Interval.from_slot(Slot(1))) + 3),
59+
GossipAggregatedAttestationStep(
60+
attestation=AggregatedAttestationSpec(
61+
validator_indices=[ValidatorIndex(0), ValidatorIndex(1)],
62+
slot=Slot(1),
63+
target_slot=Slot(1),
64+
target_root_label="block_1",
65+
),
66+
),
67+
TickStep(interval=int(Interval.from_slot(Slot(1))) + 4),
68+
GossipAggregatedAttestationStep(
69+
attestation=AggregatedAttestationSpec(
70+
validator_indices=[ValidatorIndex(1), ValidatorIndex(2)],
71+
slot=Slot(1),
72+
target_slot=Slot(1),
73+
target_root_label="block_1",
74+
),
75+
),
76+
TickStep(
77+
interval=int(Interval.from_slot(Slot(2))) + 2,
78+
checks=StoreChecks(
79+
new_pool_proof_participants={Slot(1): {0, 1, 2}},
80+
),
81+
),
82+
TickStep(interval=int(Interval.from_slot(Slot(2))) + 4),
83+
BlockStep(
84+
block=BlockSpec(slot=Slot(3), label="block_3"),
85+
checks=StoreChecks(
86+
head_slot=Slot(3),
87+
head_root_label="block_3",
88+
block_attestation_count=1,
89+
block_attestations=[
90+
AggregatedAttestationCheck(
91+
participants={0, 1, 2},
92+
attestation_slot=Slot(1),
93+
target_slot=Slot(1),
94+
),
95+
],
96+
),
97+
),
98+
],
99+
)

0 commit comments

Comments
 (0)