Skip to content

optimize gossip signature management#371

Merged
tcoratger merged 1 commit intomainfrom
fix/slow-execution
Feb 6, 2026
Merged

optimize gossip signature management#371
tcoratger merged 1 commit intomainfrom
fix/slow-execution

Conversation

@kamilsa
Copy link
Collaborator

@kamilsa kamilsa commented Feb 6, 2026

🗒️ Description

This PR optimizes the performance of the fork choice store by pruning successfully aggregated gossip signatures and fixes failing test vectors caused by the transition to a 5-interval-per-slot configuration.

Previously, signatures were kept in the map indefinitely, causing the aggregator to re-aggregate historical signatures from all previous slots every time a new slot was processed. This led to $O(N^2)$ computational complexity relative to chain length. By pruning signatures once they are successfully aggregated into a proof, we ensure each signature is processed exactly once, reducing the fill command execution time.

Updates the test_reorg_with_slot_gaps test in test_fork_choice_reorgs.py. The test had hardcoded TickStep times based on the old 4-interval-per-slot configuration. These have been updated to the current 5-interval-per-slot timing (using interval 4 for end-of-slot acceptance) to restore test correctness.

🔗 Related Issues or PRs

N/A

✅ Checklist

  • Ran tox checks to avoid unnecessary CI fails:
    uvx tox
  • Considered adding appropriate tests for the changes.
  • Considered updating the online docs in the ./docs/ directory.

@kamilsa kamilsa marked this pull request as ready for review February 6, 2026 17:51
@kamilsa kamilsa requested review from Copilot and tcoratger February 6, 2026 17:51
Copy link
Collaborator

@tcoratger tcoratger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing, thanks!

@tcoratger tcoratger merged commit 458c9be into main Feb 6, 2026
15 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to improve fork choice performance by preventing re-aggregation of historical gossip signatures (pruning them once successfully aggregated), and to update a devnet fork-choice reorg test to match the newer 5-interval-per-slot model.

Changes:

  • Prune per-validator gossip_signatures entries once they’ve been included in an aggregated proof.
  • Update test_reorg_with_slot_gaps tick timings intended to align with a 5-interval slot schedule.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
tests/consensus/devnet/fc/test_fork_choice_reorgs.py Adjusts TickStep timing intended to match 5-interval-per-slot behavior.
src/lean_spec/subspecs/forkchoice/store.py Removes successfully aggregated signatures from gossip_signatures to avoid repeated work across slots.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +394 to 395
time=(7 * 5 + 4), # Slot 7, interval 4 (acceptance)
checks=StoreChecks(
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TickStep.time is passed directly to Store.on_tick() as a Unix timestamp in seconds, but these new values treat time as if it were an (slot * intervals + interval) counter. With SECONDS_PER_SLOT=4 and INTERVALS_PER_SLOT=5, interval boundaries are 0.8s, so 7 * 5 + 4 seconds will advance the store well past slot 7 and may make the check flaky/incorrect. Consider computing the tick time from SECONDS_PER_SLOT (e.g., advance to the next slot boundary (Slot(7) + 1) * SECONDS_PER_SLOT, which will include interval-4 processing for slot 7) or otherwise deriving an actual second-based timestamp consistent with MILLISECONDS_PER_INTERVAL.

Copilot uses AI. Check for mistakes.
Comment on lines +415 to 416
time=(9 * 5 + 4), # Slot 9, interval 4 (end of slot)
checks=StoreChecks(
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: time=(9 * 5 + 4) is being treated like an interval counter, but Store.on_tick() expects seconds. With 4s slots and 5 intervals/slot, this will advance time significantly beyond slot 9. Prefer deriving the tick timestamp from SECONDS_PER_SLOT (e.g., tick to (Slot(9) + 1) * SECONDS_PER_SLOT to ensure interval-4 acceptance for slot 9 occurs during ticking).

Copilot uses AI. Check for mistakes.
Comment on lines 983 to 1001
@@ -988,7 +989,17 @@ def aggregate_committee_signatures(self) -> "Store":
if sig_key not in new_aggregated_payloads:
new_aggregated_payloads[sig_key] = []
new_aggregated_payloads[sig_key].append(aggregated_signature)
return self.model_copy(update={"latest_new_aggregated_payloads": new_aggregated_payloads})

# Prune successfully aggregated signature from gossip map
if sig_key in new_gossip_sigs:
del new_gossip_sigs[sig_key]

return self.model_copy(
update={
"latest_new_aggregated_payloads": new_aggregated_payloads,
"gossip_signatures": new_gossip_sigs,
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change alters aggregate_committee_signatures() semantics by pruning entries from gossip_signatures after aggregation. There are existing tests calling aggregate_committee_signatures() (e.g., in tests/lean_spec/subspecs/forkchoice/test_attestation_target.py) but none appear to assert the post-condition that successfully aggregated SignatureKeys are removed. Adding a focused assertion-based test would help prevent regressions (e.g., accidental re-aggregation / O(N^2) behavior returning).

Copilot uses AI. Check for mistakes.
Comment on lines 983 to 995
@@ -988,7 +989,17 @@ def aggregate_committee_signatures(self) -> "Store":
if sig_key not in new_aggregated_payloads:
new_aggregated_payloads[sig_key] = []
new_aggregated_payloads[sig_key].append(aggregated_signature)
return self.model_copy(update={"latest_new_aggregated_payloads": new_aggregated_payloads})

# Prune successfully aggregated signature from gossip map
if sig_key in new_gossip_sigs:
del new_gossip_sigs[sig_key]
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new_gossip_sigs = dict(self.gossip_signatures) copies the entire gossip signature map even when aggregated_results is empty. Given the PR goal is performance, it would be cheaper to only copy/prune when there is at least one aggregated result (or compute a keys_to_prune set first and then build a filtered dict).

Copilot uses AI. Check for mistakes.
@tcoratger tcoratger added the specs Scope: Changes to the specifications label Feb 6, 2026
@tcoratger tcoratger added this to the pq-devnet-3 milestone Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

specs Scope: Changes to the specifications

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants