feat(blockchain): cap aggregation job children to 2 (binary merge) - #482
Conversation
select_proofs_greedily picked as many existing proofs as fit the greedy coverage criterion, with no upper bound. Uncapped child counts have previously driven debug builds to stack-overflow in leanVM's rec_aggregation, and an unbounded N-way merge isn't far from leanVM's own hard cap (MAX_RECURSIONS = 16), at which point a group would fail to aggregate outright. Cap each aggregation job at 2 children, forcing pairwise (binary) merges. Proofs left uncovered by the cap stay in the new/known buffers and get merged in a later aggregation round instead of being dropped.
🤖 Kimi Code ReviewOverall Assessment: This is a well-executed safety fix that prevents stack overflow in recursive verification by capping aggregation arity to binary merges. The implementation is correct and the test coverage is thorough. Minor Suggestions
Consensus Safety Check
Code Quality
Conclusion: LGTM. The change correctly bounds recursion depth while preserving liveness (uncovered proofs remain in buffers for subsequent rounds) and safety (aggregation correctness is maintained). Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code ReviewFindings
Notes I did not see a direct memory-safety or consensus-validation bug in the narrow diff itself, but Item 1 is a real aggregation-progress regression. I could not run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewGood, confirms test helpers are valid. I now have enough context to write the review. Review: cap aggregation job children to 2 (binary merge)Overall: Sound, narrowly-scoped fix with good test coverage. The core logic in A few things worth confirming/calling out:
No correctness, security, or memory-safety issues found in the diff itself. The change is a safe, well-justified narrowing of existing behavior. Automated review by Claude (Anthropic) · sonnet · custom prompt |
Greptile SummaryThis PR changes interval-2 proof aggregation to use binary merges. The main changes are:
Confidence Score: 5/5The changed flow looks mergeable after a small cleanup to proof selection ordering.
crates/blockchain/src/aggregation.rs
|
| Filename | Overview |
|---|---|
| crates/blockchain/src/aggregation.rs | Adds the binary child cap and tests it, with one ordering issue where new proofs can fill the cap before better known proofs are considered. |
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
crates/blockchain/src/aggregation.rs:347
**Priority Bucket Hides Better Proofs**
When `new_proofs` contains two small disjoint proofs, this loop reaches the child cap before `known_proofs` is examined. A known proof for the same root that covers many more validators is skipped, so `build_job` receives a smaller `covered` set and can produce a heavier, lower-coverage aggregate even though the same two-child limit could have reused the better proof.
Reviews (1): Last reviewed commit: "feat(blockchain): cap aggregation job ch..." | Re-trigger Greptile
| @@ -337,7 +347,7 @@ fn select_proofs_greedily( | |||
| for proof_set in [new_proofs, known_proofs] { | |||
There was a problem hiding this comment.
Priority Bucket Hides Better Proofs
When new_proofs contains two small disjoint proofs, this loop reaches the child cap before known_proofs is examined. A known proof for the same root that covers many more validators is skipped, so build_job receives a smaller covered set and can produce a heavier, lower-coverage aggregate even though the same two-child limit could have reused the better proof.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/src/aggregation.rs
Line: 347
Comment:
**Priority Bucket Hides Better Proofs**
When `new_proofs` contains two small disjoint proofs, this loop reaches the child cap before `known_proofs` is examined. A known proof for the same root that covers many more validators is skipped, so `build_job` receives a smaller `covered` set and can produce a heavier, lower-coverage aggregate even though the same two-child limit could have reused the better proof.
How can I resolve this? If you propose a fix, please make it concise.…class#249) ## Motivation leanSpec PR lambdaclass#482 (merged 2025-03-25) introduces `--aggregate-subnet-ids` and changes how nodes subscribe to attestation subnets. The key architectural shift: **subnet filtering moves from the fork choice store to the P2P subscription layer**. Before this PR, ethlambda had several gaps: - Only the first validator's subnet was used (multi-validator nodes all published/subscribed to one subnet) - The store had a **hardcoded** `ATTESTATION_COMMITTEE_COUNT = 1` constant (ignored the CLI parameter) - The store performed per-attestation subnet filtering that the spec removed - No `--aggregate-subnet-ids` CLI flag existed - Non-aggregator validators didn't subscribe to their subnet (should, for gossipsub mesh health) ## Description ### Phase 1: CLI (`bin/ethlambda/src/main.rs`) - Add `--aggregate-subnet-ids` CLI flag (comma-separated, `requires = "is_aggregator"` via clap) - Collect **all** validator IDs instead of just the first one - Pass `validator_ids: Vec<u64>` and `aggregate_subnet_ids` to `SwarmConfig` ### Phase 2: P2P layer (`crates/net/p2p/src/lib.rs`, `crates/net/p2p/src/gossipsub/handler.rs`) - `SwarmConfig`: `validator_id: Option<u64>` → `validator_ids: Vec<u64>` + `aggregate_subnet_ids: Option<Vec<u64>>` - `BuiltSwarm` / `P2PServer`: single `attestation_topic` → `attestation_topics: HashMap<u64, IdentTopic>` + `attestation_committee_count: u64` - **Multi-subnet subscription logic**: - All nodes with validators subscribe to their validator subnets (for gossipsub mesh health) - Aggregators additionally subscribe to any explicitly requested `--aggregate-subnet-ids` - Aggregator with no validators and no explicit subnets: fallback to subnet 0 - Non-validator non-aggregator nodes: no attestation subscriptions - `publish_attestation`: routes per-validator to the correct subnet topic (`validator_id % committee_count`); if the subnet isn't subscribed, constructs the topic on-the-fly for gossipsub fanout - Metric `lean_attestation_committee_subnet` reports the lowest validator subnet (backward-compatible) ### Phase 3: Store simplification (`crates/blockchain/src/store.rs`) - **Removed** `ATTESTATION_COMMITTEE_COUNT` constant and `compute_subnet_id()` helper - `on_gossip_attestation`: removed `local_validator_ids` parameter; stores gossip signatures **unconditionally** (subnet filtering already handled at P2P layer) - `on_block` / `on_block_core`: removed `local_validator_ids` parameter; stores proposer signature **unconditionally** ### Phase 4: Caller updates (`crates/blockchain/src/lib.rs`, `crates/blockchain/tests/signature_spectests.rs`) - Updated all call sites to match simplified store function signatures ## How to test ```bash make fmt # ✅ passes make lint # ✅ passes make test # ✅ all 97 tests pass (forkchoice + signature + STF spec tests) ``` For devnet testing: ```bash # Single subnet (existing behavior, unchanged) --attestation-committee-count 1 --is-aggregator # Multi-subnet with explicit aggregator subscription --attestation-committee-count 2 --is-aggregator --aggregate-subnet-ids 0,1 # Multi-validator node (subscribes to all validator subnets automatically) --attestation-committee-count 4 --is-aggregator ``` Verify via logs: - Look for `Subscribed to attestation subnet` lines (one per subscribed subnet) - `Published attestation to gossipsub` now includes `subnet_id` field - Gossip signatures are stored without subnet filtering (no more silent drops) ## Notes - **Non-aggregator subscription is new behavior**: previously non-aggregators never subscribed to attestation subnets. Now they subscribe for mesh health. This slightly increases bandwidth but matches the spec. - **Hardcoded `"devnet0"` network name**: existing limitation carried forward in the fanout topic construction. Should be addressed separately. - **Subnet metric**: `lean_attestation_committee_subnet` is a single gauge. With multiple subnets, we report the lowest validator's subnet for backward compatibility. Can be improved in a follow-up. ## Related - leanSpec PR lambdaclass#482 (upstream spec change) --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
Summary
select_proofs_greedily(committee-signature aggregation worker, interval 2) picked every existing proof that added new validator coverage, with no upper bound on how many became children of one aggregation job.Why
Uncapped child counts have previously driven debug builds to stack-overflow in leanVM's
rec_aggregation, and an unbounded N-way merge approaches leanVM's own hard cap (MAX_RECURSIONS = 16), beyond which a group fails to aggregate outright. Bounding to 2 keeps recursive verification cost per round small and predictable.Scoped to the interval-2 aggregation worker (
crates/blockchain/src/aggregation.rs) only; block-building compaction and reaggregate.rs have their own greedy-selection sites left untouched.Test plan
cargo test -p ethlambda-blockchain --lib— 42 passed (3 new tests covering: cap within one set, cap across new+known combined, early-stop when no new coverage)cargo fmt --all -- --checkcargo clippy -p ethlambda-blockchain --all-targets -- -D warnings