Skip to content

Commit 4f96b22

Browse files
tcoratgerclaude
andauthored
test: vector for empty-participant aggregated gossip attestation (leanEthereum#1021)
Feed the aggregated-gossip entry point a hand-crafted aggregate carrying zero participant bits so the empty-bits rejection on that path is pinned. Honest aggregation can never produce zero participants, so the filler now honors an explicit aggregation-bits override to emit the adversarial proof. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8382476 commit 4f96b22

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,19 @@ def build_signed(
204204
validator_indices = self.validator_indices
205205
signer_indices = self.signer_indices or self.validator_indices
206206

207+
# Path 0: Raw bit override.
208+
#
209+
# Use the bits verbatim with placeholder proof bytes.
210+
# An honest aggregate can never carry zero participants, so this is the
211+
# only way to feed the gossip path an adversarial empty-bit aggregate.
212+
if self.aggregation_bits is not None:
213+
placeholder = ByteList512KiB(data=b"\x00" * 32)
214+
proof = SingleMessageAggregate(
215+
participants=self.aggregation_bits,
216+
proof=placeholder,
217+
)
218+
return SignedAggregatedAttestation(data=attestation_data, proof=proof)
219+
207220
# Path 1: Invalid signature.
208221
#
209222
# Correct participant bitfield but zeroed-out proof bytes.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Gossip aggregated attestation with an empty participant set is rejected."""
2+
3+
import pytest
4+
5+
from consensus_testing import (
6+
AggregatedAttestationSpec,
7+
BlockSpec,
8+
BlockStep,
9+
ExpectedRejection,
10+
ForkChoiceTestFiller,
11+
GossipAggregatedAttestationStep,
12+
StoreChecks,
13+
)
14+
from lean_spec.spec.forks import AggregationBits, RejectionReason, Slot
15+
16+
pytestmark = pytest.mark.valid_until("Lstar")
17+
18+
19+
def test_gossip_aggregated_attestation_empty_participants_rejected(
20+
fork_choice_test: ForkChoiceTestFiller,
21+
) -> None:
22+
"""
23+
An aggregated gossip attestation naming no participants is rejected.
24+
25+
Given
26+
-----
27+
- 4 validators.
28+
- the chain:
29+
block_1(1) -> block_2(2)
30+
31+
When
32+
----
33+
- an aggregate voting for block_2 at slot 2 carries no participants.
34+
35+
Then
36+
----
37+
- the aggregate names no validator to verify against.
38+
- validation fails with empty aggregation bits.
39+
"""
40+
fork_choice_test(
41+
steps=[
42+
BlockStep(
43+
block=BlockSpec(slot=Slot(1), label="block_1"),
44+
checks=StoreChecks(head_slot=Slot(1)),
45+
),
46+
BlockStep(
47+
block=BlockSpec(slot=Slot(2), label="block_2"),
48+
checks=StoreChecks(head_slot=Slot(2)),
49+
),
50+
GossipAggregatedAttestationStep(
51+
attestation=AggregatedAttestationSpec(
52+
validator_indices=[],
53+
aggregation_bits=AggregationBits(data=[]),
54+
slot=Slot(2),
55+
target_slot=Slot(2),
56+
target_root_label="block_2",
57+
),
58+
valid=False,
59+
expected_rejection=ExpectedRejection(
60+
reason=RejectionReason.EMPTY_AGGREGATION_BITS,
61+
message_substring=(
62+
"Aggregated attestation must reference at least one validator"
63+
),
64+
),
65+
),
66+
]
67+
)

0 commit comments

Comments
 (0)