Skip to content

Commit 653c790

Browse files
committed
lstar: do not skip trivial inputs when aggregate() serves block building
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 #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.
1 parent ebf803e commit 653c790

2 files changed

Lines changed: 25 additions & 23 deletions

File tree

packages/testing/src/consensus_testing/test_types/block_spec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ def build_signed_block_with_store(
523523
# Aggregation runs on a local clone: gossip pools mutate here, but the
524524
# caller's gossip-signature view must not be consumed by this simulated
525525
# build. Only the freshly aggregated Type-1 payloads propagate back.
526-
aggregation_store, _ = spec.aggregate(store)
526+
aggregation_store, _ = spec.aggregate(store, skip_trivial_inputs=False)
527527
merged_store = spec.accept_new_attestations(aggregation_store)
528528

529529
# Build the block through the spec's State.build_block().

src/lean_spec/forks/lstar/spec.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,13 +1603,27 @@ def update_safe_target(self, store: LstarStore) -> LstarStore:
16031603
# The head and attestation pools remain unchanged.
16041604
return store.model_copy(update={"safe_target": safe_target})
16051605

1606-
def aggregate(self, store: LstarStore) -> tuple[LstarStore, list[SignedAggregatedAttestation]]:
1606+
def aggregate(
1607+
self,
1608+
store: LstarStore,
1609+
*,
1610+
skip_trivial_inputs: bool = True,
1611+
) -> tuple[LstarStore, list[SignedAggregatedAttestation]]:
16071612
"""Turn raw validator votes into compact aggregated attestations.
16081613
16091614
Validators cast individual signatures over gossip. Before those
16101615
votes can influence fork choice or be included in a block, they
16111616
must be combined into compact cryptographic proofs.
16121617
1618+
``skip_trivial_inputs`` (default ``True``) is an **aggregator-role**
1619+
policy: when set, the ``1 raw + 0 children`` shape is skipped because
1620+
a single-validator "aggregate" carries no consensus signal beyond the
1621+
raw gossip sig already on the network (see issue #747). Interval-2
1622+
aggregator ticks use the default. Block-building callers that must
1623+
fold every chosen ``att_data`` into ``latest_known_aggregated_payloads``
1624+
(including lone gossip sigs) pass ``skip_trivial_inputs=False`` —
1625+
see ``BlockSpec`` in the consensus-testing filler.
1626+
16131627
The store holds three pools of attestation evidence:
16141628
16151629
- **Gossip signatures**: individual validator votes arriving in real-time.
@@ -1676,31 +1690,19 @@ def aggregate(self, store: LstarStore) -> tuple[LstarStore, list[SignedAggregate
16761690
if e.validator_id not in covered
16771691
]
16781692

1679-
# Skip cases where running the prover provides no consensus value:
1680-
#
1693+
# Always skip cases where there is nothing to aggregate:
16811694
# - 0 raw + 0 children: nothing to aggregate.
1682-
# - 0 raw + 1 child: a lone child proof is already a valid
1683-
# proof; nothing to do.
1684-
# - 1 raw + 0 children: a single-validator "aggregate" carries
1685-
# no information the raw gossip sig
1686-
# doesn't already carry — the sig is on
1687-
# the per-subnet `attestation_signatures`
1688-
# gossip topic at sign time, so any peer
1689-
# aggregator can fold it in as a raw
1690-
# entry next round. The recursive STARK
1691-
# prover is constant-cost in input size,
1692-
# so building a 1-validator proof spends
1693-
# the full prover budget for zero
1694-
# consensus signal. The unconsumed gossip
1695-
# sig stays in `store.attestation_signatures`
1696-
# (see bookkeeping below) and gets folded
1697-
# in by a future round once another sig
1698-
# or a child shows up.
1699-
if not child_proofs and len(raw_entries) <= 1:
1700-
continue
1695+
# - 0 raw + 1 child: a lone child proof is already valid.
17011696
if not raw_entries and len(child_proofs) < 2:
17021697
continue
17031698

1699+
# Aggregator-role optimization (``skip_trivial_inputs=True``, the
1700+
# default): skip ``1 raw + 0 children``. Block-building callers
1701+
# pass ``skip_trivial_inputs=False`` so every gossip sig they
1702+
# seeded is folded into ``latest_known_aggregated_payloads``.
1703+
if skip_trivial_inputs and not child_proofs and len(raw_entries) <= 1:
1704+
continue
1705+
17041706
# Encode raw signers as a compact bitfield when present.
17051707
# Child-only aggregation (no raw signatures) must pass None.
17061708
if raw_entries:

0 commit comments

Comments
 (0)