Skip to content

lstar/aggregate: skip trivial 1-raw-sig + 0-children case#748

Open
ch4r10t33r wants to merge 2 commits into
leanEthereum:mainfrom
ch4r10t33r:spec/skip-trivial-aggregation
Open

lstar/aggregate: skip trivial 1-raw-sig + 0-children case#748
ch4r10t33r wants to merge 2 commits into
leanEthereum:mainfrom
ch4r10t33r:spec/skip-trivial-aggregation

Conversation

@ch4r10t33r
Copy link
Copy Markdown
Collaborator

Closes #747.

Summary

Extend the early-skip predicate in lstar/spec.py:aggregate():

# Skip cases where running the prover provides no consensus value:
#   - 0 raw + 0 children: nothing to aggregate.
#   - 0 raw + 1 child:    a lone child proof is already a valid proof.
#   - 1 raw + 0 children: a single-validator "aggregate" carries no
#                         information the raw gossip sig doesn't already
#                         carry — the sig is already on the gossip topic.
if not child_proofs and len(raw_entries) <= 1:
    continue
if not raw_entries and len(child_proofs) < 2:
    continue

Why

A 1-validator aggregate can't move ⅔ quorum and the raw XMSS sig is already on the per-subnet attestation_signatures gossip topic at sign time, so any peer aggregator can fold it in as a raw entry next round. The recursive STARK prover is roughly constant-cost in input size — building a 1-validator proof costs the same as a 32-validator one.

On the multi-client devnet, two zeam aggregators sat in this case for 100% of slots over 10 minutes, spending ~10.8 s of FFI per worker run on single-validator proofs and dropping ~50% of slot triggers as in_flight. Full numbers in blockblaz/zeam#907.

Correctness

  • Wire format and verifier unchanged.
  • The lone gossip sig stays in store.attestation_signatures because the existing bookkeeping only prunes sigs whose att_data produced an aggregate. A future round folds it in once another sig or a child shows up.
  • Same end-state coverage as before, much cheaper to get there.

Tests

  • New: test_aggregate_skips_single_gossip_sig_with_no_children — asserts no aggregation is produced for the 1-sig + 0-children case and that the sig survives in the store.
  • Updated: test_multiple_attestation_data_grouped_separately — seeded with two raw sigs per att_data so each group is non-trivial. The test's intent (two distinct AttestationData → two distinct proofs) is preserved.

uv run pytest tests/lean_spec/forks/lstar/state tests/lean_spec/forks/lstar/forkchoice → 47 passed.

Related

Extend the early-skip predicate in `aggregate()` so an `AttestationData`
with exactly one raw gossip sig and no child proofs is not aggregated.

Why:

- A 1-validator "aggregate" carries no information the raw gossip sig
  doesn't already carry. The sig is on the per-subnet
  `attestation_signatures` gossip topic at sign time, so any peer
  aggregator can fold it in as a raw entry next round.
- The recursive STARK prover (`xmss/aggregation.py:aggregate`) is
  roughly constant-cost in input size — building a 1-validator proof
  costs the same as building a 32-validator one. On the multi-client
  devnet this caused two zeam aggregators to spend ~10.8 s of FFI per
  worker run on a single-validator proof, dropping ~50% of slot
  triggers as `in_flight`. See blockblaz/zeam#907.

The unconsumed gossip sig is preserved in `store.attestation_signatures`
by the existing bookkeeping (only sigs whose `att_data` produced an
aggregate are pruned), so a future round folds it in once another sig
or a child shows up.

Test changes:

- Add `test_aggregate_skips_single_gossip_sig_with_no_children` covering
  the new skip case and asserting the sig survives in the store.
- Update `test_multiple_attestation_data_grouped_separately` to seed
  two raw sigs per `att_data`. The original test put one sig per
  data, which is now (correctly) the trivial case that does not
  produce a proof; the test's intent — that two distinct
  `AttestationData` produce two distinct proofs — is preserved by
  upgrading both groups to non-trivial inputs.

Closes leanEthereum#747
The fill-ci failures were caused by applying the aggregator-role
``1 raw + 0 children`` skip unconditionally inside ``aggregate()``.
The consensus-testing ``BlockSpec`` filler calls ``spec.aggregate(store)``
before ``build_block()`` to fold gossip sigs into
``latest_known_aggregated_payloads``. Many fork-choice fixture tests seed
single-validator attestations that way; skipping them broke head selection,
reorg depth, and the MAX_ATTESTATIONS_DATA rejection case.

Add ``skip_trivial_inputs: bool = True`` to ``aggregate()``:

- Interval-2 aggregator ticks (``tick_interval``) keep the default and skip
  trivial inputs — the intended leanEthereum#747 optimization.
- Block-building simulation passes ``skip_trivial_inputs=False`` so every
  gossip sig the proposer seeded is aggregated into known payloads before
  ``build_block()`` runs.

Verified locally: all 9 previously failing fill tests pass.
@ch4r10t33r ch4r10t33r requested a review from tcoratger May 21, 2026 17:49
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.

lstar/aggregate: skip trivial 1-raw-sig + 0-children case to avoid pointless STARK proving

1 participant