feat(blockchain): gate proposer attestation aggregation behind a flag#479
Conversation
🤖 Codex Code Review
I did not find a second issue in the diff. I couldn’t run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Kimi Code ReviewOverall Assessment: The PR correctly implements a feature flag for proposer-side attestation aggregation with proper documentation, parameter propagation, and test coverage. No security vulnerabilities or consensus-critical bugs detected. Specific Feedback1. Test Maintainability (Minor)File: The new test duplicates ~60 lines of setup boilerplate (validator generation, state initialization, checkpoint construction) from existing tests like Suggestion: Extract a helper function 2. Documentation ClarityFile: The doc comment correctly notes that both aggregated and non-aggregated forms are valid per consensus rules. Consider explicitly stating that this affects only the block's 3. Parameter Propagation VerificationFile: Correctly threads
4. Consensus Safety CheckFile: The conditional logic correctly preserves the 1:1 correspondence between Note: The test at line 1009 uses 5. Performance ConsiderationsThe change correctly avoids the 6. Rust IdiomsFile: The function signature extension maintains consistency with existing parameters. Consider whether Conclusion: LGTM. Address the test helper extraction in a follow-up if desired, but not blocking. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
Greptile SummaryThis PR makes proposer-side attestation aggregation opt-in. The main changes are:
Confidence Score: 4/5The default proposal path can build a block that local import rejects before gossip.
crates/blockchain/src/block_builder.rs and the duplicate-attestation check in crates/blockchain/src/store.rs
|
| Filename | Overview |
|---|---|
| bin/ethlambda/src/cli.rs | Adds the opt-in proposer aggregation flag with a false default. |
| bin/ethlambda/src/main.rs | Passes the new CLI flag into blockchain startup. |
| crates/blockchain/src/lib.rs | Stores the proposer aggregation setting and forwards it into block production. |
| crates/blockchain/src/store.rs | Threads the flag into build_block while retaining duplicate-data rejection on import. |
| crates/blockchain/src/block_builder.rs | Skips compaction when the flag is off, which can produce duplicate-data blocks rejected by the publish path. |
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/block_builder.rs:90-92
**Duplicate Attestations Reject Proposals**
When aggregation is disabled, this branch keeps multiple selected proofs with the same `AttestationData` as separate block-body entries. The normal proposer path self-imports the built block before gossiping it, and `on_block_core` rejects duplicate `AttestationData`, so a proposer can miss its slot whenever the pool has two useful proofs for one data root.
Reviews (1): Last reviewed commit: "feat(blockchain): gate proposer attestat..." | Re-trigger Greptile
| } else { | ||
| selected | ||
| }; |
There was a problem hiding this comment.
Duplicate Attestations Reject Proposals
When aggregation is disabled, this branch keeps multiple selected proofs with the same AttestationData as separate block-body entries. The normal proposer path self-imports the built block before gossiping it, and on_block_core rejects duplicate AttestationData, so a proposer can miss its slot whenever the pool has two useful proofs for one data root.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/src/block_builder.rs
Line: 90-92
Comment:
**Duplicate Attestations Reject Proposals**
When aggregation is disabled, this branch keeps multiple selected proofs with the same `AttestationData` as separate block-body entries. The normal proposer path self-imports the built block before gossiping it, and `on_block_core` rejects duplicate `AttestationData`, so a proposer can miss its slot whenever the pool has two useful proofs for one data root.
How can I resolve this? If you propose a fix, please make it concise.
🤖 Claude Code ReviewNow I have verified findings. Let me compose the final review. Review: feat(blockchain): gate proposer attestation aggregation behind a flagThis PR adds Finding 1 — Misleading compact-phase metric recorded when no compaction runs
let compact_start = Instant::now();
let compacted = if enable_proposer_aggregation {
compact_attestations(selected, head_state)?
} else {
selected // no work done — but timer still runs
};
metrics::observe_block_proposal_phase("compact", compact_start.elapsed()); // ← always firesWith Fix: Move the timer start and observe inside the Finding 2 — Comment blocks explain mechanics (WHAT), not constraints (WHY)
CLAUDE.md: "Default to writing no comments. Only add one when the WHY is non-obvious: a hidden constraint, a subtle invariant, a workaround for a specific bug, behavior that would surprise a reader." Three added blocks restate in prose exactly what the code and parameter name already say:
That one-liner is worth keeping — it's the subtle justification that justifies the whole design. The surrounding mechanical description should be cut. Finding 3 — Static node policy threaded through two function signatures
This is a small scope PR so the current approach is defensible, but each new policy flag added this way widens two unrelated function signatures. The cleaner altitude for static node policy would be a config struct at the Summary
The correctness concern raised during review (blocks exceeding Automated review by Claude (Anthropic) · sonnet · custom prompt |
build_block unconditionally compacted attestations sharing the same AttestationData by recursively aggregating their Type-1 multi-signatures (leanSpec #510). That per-data leanVM aggregation dominates the build cost for blocks carrying attestations, yet it is not required for a valid block: the state transition unions votes by target root and the attestation-to-proof correspondence stays 1:1 whether or not duplicates are merged. Add --enable-proposer-aggregation (default off) to make this compaction opt-in. When disabled, the proposer ships the selected proofs unmerged: the block carries more attestation entries but skips the aggregation work. The flag threads CliOptions -> BlockChain::spawn -> the actor -> produce_block_with_signatures -> build_block.
The first cut left same-data entries unmerged, but a block may carry at most one entry per AttestationData: on_block rejects duplicates with StoreError::DuplicateAttestationData before signature verification, so such blocks would be invalid (and rejected by every other client too). When proposer aggregation is disabled, collapse same-data proofs by keeping the single best-coverage proof (max participant count) and dropping the rest, rather than merging them via leanVM. This preserves the one-entry-per-data invariant and the 1:1 attestation/proof correspondence while still skipping the aggregation work; the cost is lower per-entry coverage.
Bracket the compaction phase in build_block with info logs so the two paths are observable when A/B-ing the flag. The enabled path logs a start line (entries + duplicates to compact) and an end line (resulting entry count); the disabled path logs a single line noting compaction is skipped, with the same entries + duplicates counts.
Move the compaction logging into compact_attestations and keep_best_proof_per_data so each reuses the distinct-AttestationData list (order) it already builds, instead of build_block redoing the work with a separate HashSet pass. Report entries and unique counts (the unique count is order.len(), the same value the functions use to drive their fast paths) rather than a derived duplicate count.
Move the "Compacting attestations" / "Skipping attestation compaction" logs to after the no-duplicates fast-path return, and drop the log the fast path emitted. Builds whose attestations are already one-per-data (the common case, including 0/1-entry blocks) now log nothing for this phase; the logs fire only when same-data entries actually need collapsing.
4474649 to
1125e84
Compare
What
Adds
--enable-proposer-aggregation(default off) controlling howbuild_blockcollapses attestations that share the sameAttestationData.A block may carry at most one entry per
AttestationData—on_blockrejects duplicates (StoreError::DuplicateAttestationData) before signature verification, so the proposer must collapse same-data proofs to one either way. This flag only changes how:Today the proposer always aggregates; the dominant cost of building blocks that carry attestations is exactly this per-data leanVM aggregation. The flag makes it opt-in.
Threading
keep_best_proof_per_datagroups selected entries byAttestationData, keeps the entry with the most participants (ties → first occurrence), and preserves first-occurrence order — mirroringcompact_attestations' grouping but with no crypto. The one-entry-per-data invariant and the 1:1 attestation↔proof correspondence hold in both paths.Behavior change
The binary default flips: proposers stop merging same-data proofs unless
--enable-proposer-aggregationis passed. Blocks stay valid and interoperable; attestation entries just carry less voter coverage (the best single proof rather than the union).Scope note
This gates only the in-
build_blockcollapse (per the request). The separate Type-1→Type-2 merge inpropose_blockis untouched. With aggregation off there are at mostMAX_ATTESTATIONS_DATAsingle-proof components feeding that merge, same as before (one per data) — so no blow-up there. Net build-time impact is worth a devnet A/B.Tests
build_block_without_proposer_aggregation_keeps_single_best_proof_per_data: oneAttestationDatawith two proofs ({0} and {1,2}); with the flag off, asserts exactly one entry survives, 1:1 with its signature, and the kept proof is the higher-coverage{1,2}(no leanVM crypto invoked).build_blocktests passtrueto preserve their current (aggregating) behavior.cargo clippy -p ethlambda-blockchain -p ethlambda --all-targets -- -D warnings✅cargo test -p ethlambda-blockchain --lib✅ (39 passed)History
First commit took the naive "leave duplicates unmerged" approach; the follow-up fix (
keep_best_proof_per_data) corrects it after confirmingon_blockrejects duplicateAttestationData.