Conversation
…al keys) This commit introduces the dual-key model where validators have distinct attestation and proposal XMSS keys. Test fixture generation is blocked until leansig-test-keys publishes keys in the new format.
## Motivation Currently, `build_block` can produce multiple attestation entries sharing the same `AttestationData` -- each backed by a different aggregated signature proof. This happens when `extend_proofs_greedily` selects multiple proofs for the same vote data (e.g., from different aggregation intervals covering non-overlapping validator sets). [leanSpec PR #510](leanEthereum/leanSpec#510) introduces a new protocol invariant: **each unique `AttestationData` must appear at most once per block**. Multiple proofs for the same vote are compacted into a single entry, reducing block size and simplifying downstream processing. The invariant is enforced on both the building and validation sides. ## Description ### Block validation (`on_block_core`) A cheap O(n) uniqueness check is inserted **before** signature verification and state transition (the two most expensive steps). If a block contains duplicate `AttestationData` entries, it is rejected with a new `StoreError::DuplicateAttestationData` error. This uses `HashSet<&AttestationData>` -- possible because `AttestationData` already derives `Hash + Eq`. ### Block building (`build_block`) After the existing greedy proof selection loop, a new `compact_attestations` step groups all `(attestation, proof)` pairs by their `AttestationData` and merges duplicates: - **Single entry per data**: kept as-is (fast path). - **Multiple entries with empty proofs** (skip-sig / devnet mode): participant bitfields are unioned via `union_aggregation_bits`, producing a single `AggregatedSignatureProof::empty(merged_bits)`. - **Multiple entries with real proofs** (production with XMSS aggregation): the proof covering the most participants is kept. Full merge would require recursive proof aggregation, which lean-multisig does not yet support (the [spec itself notes](https://github.com/leanEthereum/leanSpec/blob/main/src/lean_spec/subspecs/xmss/aggregation.py#L72) "The API supports recursive aggregation but the bindings currently do not"). The intermediate block built inside the justification-check loop is **not** compacted -- it only tests whether justification advances, and vote counting is identical regardless of entry layout. ### New helpers | Function | Purpose | |----------|---------| | `union_aggregation_bits(a, b)` | Bitwise OR of two `AggregationBits` bitfields | | `compact_attestations(atts, proofs)` | Groups by `AttestationData`, merges duplicates, preserves first-occurrence order | ### Future work When lean-multisig adds recursive aggregation support, the `else` branch in `compact_attestations` (real proofs) can be upgraded to cryptographically merge all proofs instead of keeping only the best one. This will recover the small amount of validator coverage currently lost when multiple real proofs exist for the same `AttestationData`. ## Test plan - [x] `compact_attestations_no_duplicates` -- distinct data passes through unchanged - [x] `compact_attestations_merges_empty_proofs` -- two entries with same data + empty proofs merge into one with unioned participants covering all validators - [x] `compact_attestations_real_proofs_keeps_best` -- two entries with same data + real proofs keeps the one with more participants - [x] `compact_attestations_preserves_order` -- multiple data entries (some duplicated) output in first-occurrence order - [x] `on_block_rejects_duplicate_attestation_data` -- block with duplicate entries returns `DuplicateAttestationData` error via `on_block_without_verification` - [x] All 18 existing blockchain lib tests still pass - [x] `cargo fmt --all -- --check` clean - [x] `cargo clippy -p ethlambda-blockchain -- -D warnings` clean - [ ] Spec test fixtures update (deferred until leanSpec submodule includes PR #510) --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
… (incl. #275 #277) ## Motivation Implements devnet4 ([leanSpec#449](leanEthereum/leanSpec#449)): separate attestation and proposal keys for validators, replacing the single-key model and removing the proposer attestation wrapper. ## Description ### Phase 1: Types (#230) - `Validator` gains `attestation_pubkey` + `proposal_pubkey` (replaces single `pubkey`) - `SignedBlockWithAttestation` → `SignedBlock`, `BlockWithAttestation` deleted - Genesis config updated to dual-key YAML format ### Phase 2: Key manager + block proposal (#231) - `ValidatorKeyPair` with separate attestation/proposal secret keys - Block proposal signs `hash_tree_root(block)` with proposal key (no proposer attestation) - All validators now attest at interval 1 (proposer no longer skipped) ### Phase 3: Store + verification (#232) - Signature verification uses `get_attestation_pubkey()` / `get_proposal_pubkey()` as appropriate - Removed ~40 lines of proposer attestation handling from `on_block_core` - Storage reads/writes `SignedBlock` directly ### Phase 4: Network + tests (#233) - Type rename cascade across all network layer files - Test harness updated: removed `ProposerAttestation`, simplified `build_signed_block()` - Added proposal signing metrics - leanSpec bumped to 9c30436 (dual-key test fixtures) - Skipped `test_reorg_with_slot_gaps` (fixture relies on gossip proposer attestation state) ## Supersedes Closes #230, closes #231, closes #232 ## Blockers - **lean-quickstart**: needs devnet4 genesis config support for manual devnet testing --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
🤖 Kimi Code ReviewReview SummaryThis PR implements devnet4 compatibility for ethlambda, introducing a dual XMSS key system (separate attestation and proposal keys per validator) and simplifying the block structure by removing the proposer attestation wrapper. The changes are extensive but well-structured. Critical Consensus-Layer Changes1. Dual Key Architecture (Major)Files:
Line-specific notes:
2. Block Structure SimplificationFiles:
3. Attestation Compaction & LimitsFiles:
Security consideration: The duplicate check prevents validators from including the same attestation data multiple times to artificially inflate block weight or exploit fork choice edge cases. Cryptographic UpdatesXMSS Scheme UpgradeFiles:
Verification: The new scheme uses aborting hypercube message hash which is safer against certain forgery attacks. Network & StorageP2P Protocol UpdatesFiles:
Storage SchemaFiles:
Testing & Spec ComplianceTest Fixture UpdatesFiles:
Fork Choice Test RunnerFiles:
Minor Issues & Suggestions1. Error Message ClarityFile: return Err(StoreError::DuplicateAttestationData {
count: attestations.len(),
unique: seen.len(),
});The error counts total attestations vs unique, but the check happens after seeing a duplicate. Consider reporting which specific 2. Key Loading ValidationFile: 3. Comment AccuracyFile: 4. SSZ Test DataFile: Security Assessment
ConclusionThis PR is ready for merge pending resolution of minor test data regeneration. The dual-key architecture is correctly implemented, the block structure simplification removes technical debt, and all consensus-critical paths (fork choice, justification, finalization) are properly updated. Recommended follow-ups:
Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code Review
I did not run tests successfully because Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewHere is the code review for PR #283: PR OverviewThis PR migrates ethlambda to the devnet4 protocol by:
The dual-key design and the block-root signing change are architecturally sound. Critical Issues
// New
let (_sorted_pubkeys, aggregate) = xmss_aggregate(&[], raw_xmss, &message.0, slot, 2);
let serialized = aggregate.serialize();
// When merging duplicates:
compacted_proofs.push(AggregatedSignatureProof::empty(merged_bits.clone()));Any non-empty Notable Concerns
leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" }Any push to
The comment acknowledges this: "leansig doesn't expose any pk getters." A validator misconfiguring attester and proposer key files swapped would sign with the wrong key and produce invalid blocks, with no detection at startup. This is a known limitation, but it should be tracked as a follow-up issue since it's a silent misconfiguration surface. Cross-client XMSS compatibility tests removed without replacement ( The ream cross-client test vectors (4 tests) were removed. This is expected because the signature scheme changed, but there are now zero cross-client signature compatibility tests. New test vectors from ream / zeam / grandine against the devnet4 scheme should be added before this PR merges. Minor Issues
// Before: iterates post_state.justified_slots.len()
// After:
let post_slots: Vec<bool> = (0..justified_slots.data.len())
.map(|i| post_state.justified_slots.get(i) == Some(true))
.collect();If the state's bitfield is longer than the fixture (
The test creates
.map_err(|_| D::Error::custom(format!("pubkey is not valid hex: {s}")))?;The old per-array-index message (
Good Changes
Automated review by Claude (Anthropic) · sonnet · custom prompt |
This PR bumps the libp2p version to 2f14d0ec9665a01cfb6a02326c90628c4bba521c (the commit is in our fork). # Changelog Here's the summary of meaningful changes from upstream `master`: ## Gossipsub Changes (likely fixed our issue) ### `5d47d9d` - Port of 55e4a64 (biggest change) **Multiple gossipsub fixes to `Instant` arithmetic and backoff handling:** - **GRAFT flood penalty fix**: Replaced unsafe `Instant` subtraction (which can panic/overflow) with `checked_sub` + `saturating_duration_since`. The old code computed `(backoff_time + graft_flood_threshold) - prune_backoff` which could panic if the arithmetic overflowed. This is likely **the fix** that resolved cross-client mesh issues: if a peer's GRAFT was incorrectly penalized due to arithmetic overflow, it would never join the mesh. - **IWANT followup time**: Added `checked_add` to prevent `Instant` overflow - **Fanout TTL check**: Replaced `Instant` addition with `saturating_duration_since` - **IDONTWANT timeout**: Same pattern, safer arithmetic - **Max PRUNE backoff cap**: Added `MAX_REMOTE_PRUNE_BACKOFF_SECONDS = 3600` to prevent a remote peer from requesting an absurdly long backoff ### `a7d59cb` - CVE fix (GHSA-gc42-3jg7-rxr2) **Security fix**: Ignore oversized PRUNE backoff values. A malicious peer could send a PRUNE with a backoff duration so large that `Instant::now() + time` would overflow, causing a panic. Now uses `checked_add` and ignores invalid values. ### `7637c23` - Optimize IDONTWANT send Only send IDONTWANT for first-seen large messages, deduplicating redundant messages. ### `aa7a9ec` - Partial messages extension New gossipsub feature for partial message delivery (spec: libp2p/specs#704). ### `055186d` - Fix duplicate metrics Bug fix for double-counted metrics. ## Other Changes - `8541b83` - Remove `async_trait` from request_response (this caused our codec.rs compile fix) - `b6b79b2` - MSRV bump to 1.88.0, Rust edition 2024 - `aad1f8e` - Remove unused `rpc.rs` - `7cbf7c1` - TLS key logging via SSLKEYLOGFILE - `3f88b30` - Rendezvous protocol port - ~35 dependency bumps ## Root Cause Analysis The GRAFT flood penalty fix in `5d47d9d` is almost certainly what fixed our cross-client block propagation. The old code had unsafe `Instant` arithmetic that could overflow when zeam peers (with slightly different timing) sent GRAFT requests. The overflow would cause the penalty check to always trigger, causing ethlambda to PRUNE zeam peers from the block topic mesh. Attestations worked because they used fanout (bypasses mesh/GRAFT entirely).
This PR adds devnet 4 support to ethlambda, removing devnet 3 support.