Skip to content

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

Description

@ch4r10t33r

Problem

lstar/spec.py aggregate() currently skips an AttestationData only when there are zero raw sigs and fewer than two children:

# A lone child proof is already a valid proof — nothing to do.
if not raw_entries and len(child_proofs) < 2:
    continue

So with 1 raw sig + 0 children, the spec aggregates and produces a SignedAggregatedAttestation whose proof is a recursive STARK over a single signature.

That work is wasted:

  • The proof contains exactly one validator. It cannot move ⅔ quorum, so it gives consensus no signal it didn't already have.
  • The raw XMSS sig is already on the per-subnet attestation_signatures gossip topic at sign time. 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 proof.

What we measured

On the multi-client devnet, two zeam aggregators (one validator each on their duty subnet, no peer payloads arriving in latest_new_aggregated_payloads) sat in the 1 raw + 0 children case for 100% of slots over a 10-minute window. The result:

  • Each aggregator spent ~10.8 s of FFI per worker run on a 1-validator "aggregate".
  • ~50% of slot triggers got dropped as in_flight because the worker hadn't finished by the next slot.
  • Every published aggregate covered exactly one validator.

Full numbers: blockblaz/zeam#907.

Proposed change

Extend the skip predicate in aggregate():

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

Bookkeeping below the loop already keeps gossip sigs that were not consumed by an aggregation, so the lone sig stays in store.attestation_signatures and gets folded in by a future round once raw_entries >= 2 or any child shows up.

Why this is correctness-preserving

  • The wire format and verifier are unchanged.
  • The raw sig stays on the gossip topic — peers don't lose visibility of the vote.
  • The store invariant (consumed sigs are pruned, untouched sigs remain) is unchanged.
  • A spec-faithful client today produces a 1-validator aggregate that another aggregator could fold in as a child next round. With the change, peers still see the raw sig and fold it in directly. Same coverage, much cheaper.

Acceptance criteria

  • aggregate() returns no aggregation for the 1 raw + 0 children case.
  • Existing aggregation tests that happen to land on 1 raw + 0 children are updated to >= 2 raw or include a child, with an explanatory comment.
  • A new test asserts that aggregate() on a store with a single gossip sig and empty payload maps produces zero aggregations and leaves the sig in store.attestation_signatures for a future pass.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions