Skip to content

refactor(testing): merge the verify-proof fixtures and deduplicate aggregation#850

Merged
tcoratger merged 1 commit into
leanEthereum:mainfrom
tcoratger:refactor/merge-verify-proof-fixtures
Jun 5, 2026
Merged

refactor(testing): merge the verify-proof fixtures and deduplicate aggregation#850
tcoratger merged 1 commit into
leanEthereum:mainfrom
tcoratger:refactor/merge-verify-proof-fixtures

Conversation

@tcoratger
Copy link
Copy Markdown
Collaborator

Motivation

Item 3 of the packages/testing/ audit (follow-up to #848, #849). Two findings:

  1. verify_single_message_proofs.py and verify_multi_message_proofs.py duplicated the ALTERNATE_HEAD_ROOT sentinel, three tamper classes, and the false-positive swap guard — single-message is the degenerate component_index == 0 case of multi-message.
  2. The identical leaf-aggregation idiom (SingleMessageAggregate.aggregate(children=[], raw_xmss=zip(indices, keys, signatures), ...)) appeared 4 times: in both verify fixtures and twice in keys.py.

Changes

Two fixture files → one (test_fixtures/verify_proofs.py):

  • The sentinel, the component-index guard, the alternate-head rebind, and the participant-key-swap false-positive guard each exist exactly once
  • One shared tamper vocabulary of five classes, where component_index: int = 0 — single-message tests stay terse (RebindToAlternateHeadRoot()), multi-message tests are explicit (RebindToAlternateHeadRoot(component_index=1)). The single fixture validates component_index == 0 and its union excludes the two inherently multi-component tampers
  • Renames (no aliases, per the no-backward-compat rule): RebindComponentToAlternateHeadRootRebindToAlternateHeadRoot, IncrementComponentSlotIncrementEmittedSlot, SwapComponentParticipantPublicKeySwapParticipantPublicKey, SwapComponentMessageBindingsSwapMessageBindings, DropComponentMessageBindingDropMessageBinding; the single spec's bare index field becomes participant_index

4× aggregation copy → 1: XmssKeyManager.sign_and_aggregate already implemented the idiom, so it gains an optional precomputed_signatures lookup and all four sites collapse onto it — both fixture helpers (_aggregate_flat, _single_message_aggregate) are deleted, the child-group pre-aggregation in the recursive path delegates too, and build_attestation_proofs becomes a thin comprehension over it. The only remaining raw aggregate(...) call is the two-level recursive fold, which is genuinely unique.

Vector impact: none

The fixtures emit the same client-visible fields and the aggregation inputs are byte-identical (sign_and_aggregate fetches the same public keys the fixtures fetched themselves). Tamper objects are exclude=True and never reach JSON.

Verification

  • just check passes (ruff, format, ty, codespell, mdformat)
  • Smoke fill tests/consensus/lstar/verify_proofs: 21/21 pass (all tamper paths, including the renamed multi-message ones)
  • Smoke fill tests/consensus/lstar/state_transition/test_justification.py: 21/21 pass (exercises the rewritten build_attestation_proofs + precomputed-signature path in block building)

🤖 Generated with Claude Code

…gregation

The two proof-verification fixture files duplicated the alternate-head
sentinel, three tamper classes, and the false-positive swap guard.
The single-message vector is the degenerate one-component case of the
multi-message vector, so the tamper vocabulary is now shared: one set
of five tamper classes where component_index defaults to 0, with the
single-message fixture accepting the three that apply to it.

The identical leaf-aggregation idiom (sign each validator, zip with
public keys, aggregate into a single-message proof) appeared four
times: twice across the verify fixtures and twice in the key manager.
The existing sign-and-aggregate method already implemented it, so it
gains an optional precomputed-signature lookup and all four sites
collapse onto it. The only remaining raw aggregation call is the
recursive two-level fold, which is genuinely unique.

Changes:

- one verify_proofs module replaces the two fixture files; the
  sentinel, tamper classes, component-index guard, alternate-head
  rebind, and participant-key swap guard exist once
- tamper renames: the Component-prefixed variants disappear
  (RebindComponentToAlternateHeadRoot -> RebindToAlternateHeadRoot,
  IncrementComponentSlot -> IncrementEmittedSlot,
  SwapComponentParticipantPublicKey -> SwapParticipantPublicKey,
  SwapComponentMessageBindings -> SwapMessageBindings,
  DropComponentMessageBinding -> DropMessageBinding); the single
  spec's bare index field becomes participant_index
- the key manager's attestation-proof builder delegates to
  sign-and-aggregate instead of repeating the aggregation idiom

Generated vectors are unchanged: the fixtures emit the same fields
and the aggregation inputs are byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tcoratger tcoratger merged commit 8c9378a into leanEthereum:main Jun 5, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant