Skip to content

Commit d9df901

Browse files
unnawuttcoratgerclaude
authored
test: add test vectors for verifying multi message proofs (leanEthereum#810)
* test(testing): add VerifyMultiMessageProofsTest fixture Mirror the single-message fixture for the Type-2 multi-message aggregate primitive: per-component validator lists, per-component attestation data, emitted parallel lists of messages, slots, public keys, and aggregation bits, plus the merged proof bytes. Three tampers target one component at a time: - RebindComponentToAlternateHeadRoot regenerates one component against an alternate head root and re-merges, so the emitted layout stays honest but the merged proof bytes carry the off-target binding. - IncrementComponentSlot bumps one component's emitted slot past its bound slot. - SwapComponentParticipantPublicKey swaps one participant's key for another validator's, breaking the layout the merged proof verifies against. * test(consensus): add multi-message verify vectors for lstar Three positive vectors covering two- and three-component bundles with single-, four-, and mixed-sized participant lists; three rejection vectors covering wrong message, wrong slot, and wrong public key applied to one component at a time. * test(testing): cover multi-message-specific rejection paths and dedup the verify ladder Add the failure modes that only a multi-message (Type-2) proof can suffer, which the initial vector set did not reach: - SwapComponentMessageBindings transposes two components' emitted message-slot bindings after an honest merge, with distinct head roots per component, so each component's proof faces the other's binding. This is the canonical Type-2 attack the positional binding exists to reject. - DropComponentMessageBinding removes one component's binding while keeping its keys, exercising the verifier's binding-count guard. Add two valid vectors for parity with the single-message suite: a single component bundle (the n=1 boundary) and a non-contiguous committee whose aggregation bits resolve to [1, 0, 1, 1]. Factor the identical expectation-comparison ladder into BaseConsensusFixture.assert_expected_outcome and reuse it from both verify fixtures. Hoist the repeated component-index range check into a helper, and guard the slot-increment tamper against landing on a neighbour's slot. Inline the per-test attestation data to match the single-message sibling files, rename the ambiguous swap field to participant_index, and correct the union docstring. 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 c2b7d07 commit d9df901

7 files changed

Lines changed: 685 additions & 13 deletions

File tree

packages/testing/src/consensus_testing/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,24 @@
77
from .test_fixtures import (
88
ApiEndpointTest,
99
BaseConsensusFixture,
10+
DropComponentMessageBinding,
1011
ForkChoiceTest,
1112
GossipsubHandlerTest,
13+
IncrementComponentSlot,
1214
IncrementEmittedSlot,
1315
JustifiabilityTest,
1416
NetworkingCodecTest,
1517
PoseidonPermutationTest,
18+
RebindComponentToAlternateHeadRoot,
1619
RebindToAlternateHeadRoot,
1720
SlotClockTest,
1821
SSZTest,
1922
StateTransitionTest,
23+
SwapComponentMessageBindings,
24+
SwapComponentParticipantPublicKey,
2025
SwapParticipantPublicKey,
2126
SyncTest,
27+
VerifyMultiMessageProofsTest,
2228
VerifySignaturesTest,
2329
VerifySingleMessageProofsTest,
2430
)
@@ -42,6 +48,7 @@
4248
StateTransitionTestFiller = Type[StateTransitionTest]
4349
ForkChoiceTestFiller = Type[ForkChoiceTest]
4450
VerifySingleMessageProofsTestFiller = Type[VerifySingleMessageProofsTest]
51+
VerifyMultiMessageProofsTestFiller = Type[VerifyMultiMessageProofsTest]
4552
VerifySignaturesTestFiller = Type[VerifySignaturesTest]
4653
SSZTestFiller = Type[SSZTest]
4754
NetworkingCodecTestFiller = Type[NetworkingCodecTest]
@@ -70,6 +77,12 @@
7077
"RebindToAlternateHeadRoot",
7178
"IncrementEmittedSlot",
7279
"SwapParticipantPublicKey",
80+
"VerifyMultiMessageProofsTest",
81+
"RebindComponentToAlternateHeadRoot",
82+
"IncrementComponentSlot",
83+
"SwapComponentParticipantPublicKey",
84+
"SwapComponentMessageBindings",
85+
"DropComponentMessageBinding",
7386
"VerifySignaturesTest",
7487
"SSZTest",
7588
"NetworkingCodecTest",
@@ -94,6 +107,7 @@
94107
"StateTransitionTestFiller",
95108
"ForkChoiceTestFiller",
96109
"VerifySingleMessageProofsTestFiller",
110+
"VerifyMultiMessageProofsTestFiller",
97111
"VerifySignaturesTestFiller",
98112
"SSZTestFiller",
99113
"NetworkingCodecTestFiller",

packages/testing/src/consensus_testing/test_fixtures/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
from .ssz import SSZTest
1212
from .state_transition import StateTransitionTest
1313
from .sync import SyncTest
14+
from .verify_multi_message_proofs import (
15+
DropComponentMessageBinding,
16+
IncrementComponentSlot,
17+
RebindComponentToAlternateHeadRoot,
18+
SwapComponentMessageBindings,
19+
SwapComponentParticipantPublicKey,
20+
VerifyMultiMessageProofsTest,
21+
)
1422
from .verify_signatures import VerifySignaturesTest
1523
from .verify_single_message_proofs import (
1624
IncrementEmittedSlot,
@@ -27,6 +35,12 @@
2735
"RebindToAlternateHeadRoot",
2836
"IncrementEmittedSlot",
2937
"SwapParticipantPublicKey",
38+
"VerifyMultiMessageProofsTest",
39+
"RebindComponentToAlternateHeadRoot",
40+
"IncrementComponentSlot",
41+
"SwapComponentParticipantPublicKey",
42+
"SwapComponentMessageBindings",
43+
"DropComponentMessageBinding",
3044
"VerifySignaturesTest",
3145
"SSZTest",
3246
"NetworkingCodecTest",

packages/testing/src/consensus_testing/test_fixtures/base.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,31 @@ def serialize_exception(self, value: type[Exception] | None) -> str | None:
4444
if value is None:
4545
return None
4646
return value.__name__
47+
48+
def assert_expected_outcome(self, exception_raised: Exception | None) -> None:
49+
"""Compare a self-verification outcome against the configured expectation.
50+
51+
A fixture that self-verifies its own output catches the verifier exception.
52+
It then hands the caught exception here to decide pass or fail.
53+
54+
Args:
55+
exception_raised: The exception the verifier raised, or None on success.
56+
57+
Raises:
58+
AssertionError: When the outcome disagrees with the expectation.
59+
"""
60+
# No expectation means the bundle is honest and must verify.
61+
if self.expect_exception is None:
62+
if exception_raised is not None:
63+
raise AssertionError(f"Verifier rejected an honest bundle: {exception_raised}")
64+
# An expectation that produced no exception means the tamper went undetected.
65+
elif exception_raised is None:
66+
raise AssertionError(
67+
f"Expected {self.expect_exception.__name__} but verification succeeded"
68+
)
69+
# A wrong exception type means the rejection fired for the wrong reason.
70+
elif not isinstance(exception_raised, self.expect_exception):
71+
raise AssertionError(
72+
f"Expected {self.expect_exception.__name__} but got "
73+
f"{type(exception_raised).__name__}: {exception_raised}"
74+
)

0 commit comments

Comments
 (0)