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
Related
Problem
lstar/spec.py aggregate()currently skips anAttestationDataonly when there are zero raw sigs and fewer than two children:So with
1 raw sig + 0 children, the spec aggregates and produces aSignedAggregatedAttestationwhose proof is a recursive STARK over a single signature.That work is wasted:
attestation_signaturesgossip topic at sign time. Any peer aggregator can fold it in as a raw entry next round.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 the1 raw + 0 childrencase for 100% of slots over a 10-minute window. The result:in_flightbecause the worker hadn't finished by the next slot.Full numbers: blockblaz/zeam#907.
Proposed change
Extend the skip predicate in
aggregate():Bookkeeping below the loop already keeps gossip sigs that were not consumed by an aggregation, so the lone sig stays in
store.attestation_signaturesand gets folded in by a future round onceraw_entries >= 2or any child shows up.Why this is correctness-preserving
Acceptance criteria
aggregate()returns no aggregation for the1 raw + 0 childrencase.1 raw + 0 childrenare updated to>= 2 rawor include a child, with an explanatory comment.aggregate()on a store with a single gossip sig and empty payload maps produces zero aggregations and leaves the sig instore.attestation_signaturesfor a future pass.Related