Skip to content

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

@ch4r10t33r

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions