Skip to content

Commit a8a9f9e

Browse files
committed
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.
1 parent 8e28a19 commit a8a9f9e

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

tests/consensus/lstar/fc/test_block_production.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,101 @@ def test_produce_block_includes_pending_attestations(
415415
)
416416

417417

418+
def test_block_builder_merges_proofs_to_attestation(
419+
fork_choice_test: ForkChoiceTestFiller,
420+
) -> None:
421+
"""
422+
Two overlapping proofs for the same data merge into one in-block attestation.
423+
424+
Scenario
425+
--------
426+
Chain: genesis(0) -> block_1(1) -> block_2(2)
427+
428+
At slot 2, interval 3 (past the aggregate phase), two separate aggregators
429+
each gossip an aggregated attestation for the same AttestationData over
430+
slot_2 -> block_2:
431+
432+
- aggregator A: validators {0, 1, 2}
433+
- aggregator B: validators {1, 2, 3} (overlaps A on {1, 2})
434+
435+
Gossiping after interval 2 stops the local aggregator from absorbing them
436+
on this slot. Both proofs land in the new pool side by side.
437+
Slot 2, interval 4 then migrates both into the known pool.
438+
439+
Slot 3 block production reads from the known pool.
440+
The greedy picker takes the first proof, then takes the second because it
441+
still adds one uncovered validator on top of the first.
442+
The builder merges the two proofs into one via SingleMessageAggregate.aggregate
443+
with two children.
444+
445+
Expected
446+
--------
447+
- 1 aggregated attestation in the block body.
448+
- Participants = {0, 1, 2, 3} (union of the two source proofs).
449+
"""
450+
# Slot 2 interval 3 sits after the aggregate phase but before acceptance.
451+
# Gossiped aggregates accumulate in the new pool without local pre-merging.
452+
after_aggregate_interval = 2 * int(INTERVALS_PER_SLOT) + 3
453+
after_aggregate_time = math.ceil(
454+
after_aggregate_interval * int(MILLISECONDS_PER_INTERVAL) / 1000
455+
)
456+
# Slot 3 boundary. Crosses slot 2 interval 4, which migrates new -> known.
457+
next_slot_time = 3 * int(SECONDS_PER_SLOT)
458+
459+
fork_choice_test(
460+
steps=[
461+
BlockStep(
462+
block=BlockSpec(slot=Slot(1), label="block_1"),
463+
checks=StoreChecks(head_slot=Slot(1)),
464+
),
465+
BlockStep(
466+
block=BlockSpec(slot=Slot(2), label="block_2"),
467+
checks=StoreChecks(head_slot=Slot(2)),
468+
),
469+
TickStep(time=after_aggregate_time),
470+
GossipAggregatedAttestationStep(
471+
attestation=AggregatedAttestationSpec(
472+
validator_indices=[
473+
ValidatorIndex(0),
474+
ValidatorIndex(1),
475+
ValidatorIndex(2),
476+
],
477+
slot=Slot(2),
478+
target_slot=Slot(2),
479+
target_root_label="block_2",
480+
),
481+
),
482+
GossipAggregatedAttestationStep(
483+
attestation=AggregatedAttestationSpec(
484+
validator_indices=[
485+
ValidatorIndex(1),
486+
ValidatorIndex(2),
487+
ValidatorIndex(3),
488+
],
489+
slot=Slot(2),
490+
target_slot=Slot(2),
491+
target_root_label="block_2",
492+
),
493+
),
494+
TickStep(time=next_slot_time),
495+
BlockStep(
496+
block=BlockSpec(slot=Slot(3), label="block_3"),
497+
checks=StoreChecks(
498+
head_slot=Slot(3),
499+
block_attestation_count=1,
500+
block_attestations=[
501+
AggregatedAttestationCheck(
502+
participants={0, 1, 2, 3},
503+
attestation_slot=Slot(2),
504+
target_slot=Slot(2),
505+
),
506+
],
507+
),
508+
),
509+
],
510+
)
511+
512+
418513
def test_block_builder_recovers_finality_after_non_zero_boundary_stall(
419514
fork_choice_test: ForkChoiceTestFiller,
420515
) -> None:

0 commit comments

Comments
 (0)