Skip to content

feat: add devnet4 support#283

Open
MegaRedHand wants to merge 11 commits intomainfrom
devnet4
Open

feat: add devnet4 support#283
MegaRedHand wants to merge 11 commits intomainfrom
devnet4

Conversation

@MegaRedHand
Copy link
Copy Markdown
Collaborator

@MegaRedHand MegaRedHand commented Apr 14, 2026

This PR adds devnet 4 support to ethlambda, removing devnet 3 support.

pablodeymo and others added 7 commits April 1, 2026 19:16
…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>
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

Review Summary

This 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 Changes

1. Dual Key Architecture (Major)

Files: crates/common/types/src/state.rs, crates/blockchain/src/key_manager.rs, bin/ethlambda/src/main.rs

  • Change: Each validator now has independent attestation_pubkey and proposal_pubkey (52 bytes each).
  • Security: Correctly isolates attestation and proposal signing domains, preventing OTS (one-time signature) exhaustion attacks where a validator might accidentally reuse a key.
  • Implementation: ValidatorKeyPair struct properly manages both keys with independent XMSS window advancement (sign_with_attestation_key vs sign_with_proposal_key).

Line-specific notes:

  • crates/blockchain/src/key_manager.rs:23-30 - Good documentation explaining why dual keys are needed.
  • crates/blockchain/src/key_manager.rs:83-103 and crates/blockchain/src/key_manager.rs:120-140 - Both keys correctly advance their preparation windows independently. This is critical for validators to sign both attestations and blocks in the same slot.

2. Block Structure Simplification

Files: crates/common/types/src/block.rs, crates/blockchain/src/lib.rs, crates/blockchain/src/store.rs

  • Change: Removed BlockWithAttestation wrapper and proposer_attestation field. Proposer now signs block_root directly with proposal key.
  • Consensus Impact:
    • Breaking change in SSZ schema (SignedBlock replaces SignedBlockWithAttestation).
    • Proposer attestations are now treated as regular gossip attestations, eliminating circular weight advantage in fork choice (proposer no longer gets "free" attestation weight in their own block).
  • Verification: verify_signatures in crates/blockchain/src/store.rs:1176-1198 correctly uses:
    • get_attestation_pubkey() for attestation verification
    • get_proposal_pubkey() for block proposer signature verification

3. Attestation Compaction & Limits

Files: crates/blockchain/src/store.rs

  • New: MAX_ATTESTATIONS_DATA = 16 limit per block (leanSpec #536 compliance).
  • New: compact_attestations function merges duplicate AttestationData entries by unioning aggregation bits.
  • Validation: on_block_core checks for duplicates using HashSet (lines 569-580).

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 Updates

XMSS Scheme Upgrade

Files: crates/common/types/src/signature.rs, crates/common/crypto/src/lib.rs

  • Change: Updated from lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8 to instantiations_aborting::lifetime_2_to_the_32::SchemeAbortingTargetSumLifetime32Dim46Base8.
  • Signature size: Reduced from 3112 to 2536 bytes (correct for new scheme).
  • Aggregation: Updated to use xmss_aggregate with log_inv_rate=2 (cross-client compatibility with zeam, ream, lantern).

Verification: The new scheme uses aborting hypercube message hash which is safer against certain forgery attacks.


Network & Storage

P2P Protocol Updates

Files: crates/net/p2p/src/gossipsub/handler.rs, crates/net/p2p/src/req_resp/

  • Change: All references updated from SignedBlockWithAttestation to SignedBlock.
  • Encoding: SSZ encoding remains compatible (same field order), but wire format changes due to removed wrapper.

Storage Schema

Files: crates/storage/src/store.rs

  • Change: BlockSignaturesWithAttestation removed; now stores BlockSignatures directly.
  • Migration: Existing databases will need regeneration (breaking change acceptable for devnet).

Testing & Spec Compliance

Test Fixture Updates

Files: crates/blockchain/tests/, crates/common/test-fixtures/src/lib.rs

  • Good: Test validators now include both attestationPubkey and proposalPubkey in YAML fixtures.
  • Good: on_gossip_attestation_without_verification added for fork choice spec tests that don't include signature data.

Fork Choice Test Runner

Files: crates/blockchain/tests/forkchoice_spectests.rs

  • Note: Test runner now resolves headRootLabel to headRoot for checks (lines 196-210), handling the fixture format where labels reference previously seen blocks.

Minor Issues & Suggestions

1. Error Message Clarity

File: crates/blockchain/src/store.rs:578-580

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 AttestationData was duplicated for easier debugging.

2. Key Loading Validation

File: bin/ethlambda/src/main.rs:271-286
The classify_role function correctly parses filenames for "attester" and "proposer" substrings. This matches zeam and lantern conventions, but consider making this more robust with explicit metadata in the YAML rather than filename parsing.

3. Comment Accuracy

File: crates/blockchain/src/store.rs:1179-1183
The comment mentions "bounds check in a single pass" but the code uses .map() followed by .collect::<Result<_, _>>(). This is fine but the comment is slightly misleading—it's not a single pass in the traditional sense, though it is iterator-based.

4. SSZ Test Data

File: crates/net/p2p/test_data/signed_block.ssz
The test file is renamed but the test in encoding.rs is ignored with comment "needs devnet4 block". Ensure test data is regenerated before merging.


Security Assessment

Component Assessment Notes
Key Separation ✅ Secure Proper isolation prevents cross-protocol attacks
XMSS Window ✅ Correct Both keys advance independently, preventing exhaustion
Signature Verification ✅ Correct Uses correct pubkey type per signature purpose
Attestation Limits ✅ Compliant MAX_ATTESTATIONS_DATA prevents DoS
Duplicate Detection ✅ Secure Prevents attestation stuffing attacks

Conclusion

This 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:

  1. Regenerate signed_block.ssz test fixture.
  2. Update documentation (CLAUDE.md changes look good, but verify all diagrams).
  3. Run full sync test against devnet4 before tagging release.

Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. store.rs and store.rs: compact_attestations is cryptographically invalid. When multiple proofs for the same AttestationData are selected, it unions the participant bits but discards the real proof bytes and replaces them with AggregatedSignatureProof::empty(...). The resulting SignedBlock cannot pass verify_aggregated_signature for the merged participant set, so any block that actually hits this path will be produced invalid and rejected by other nodes. This needs either “at most one proof per AttestationData” or real re-aggregation of proofs, not bitset union plus an empty proof.

  2. store.rs, handler.rs, and codec.rs: the PR changes the SSZ shape from SignedBlockWithAttestation / BlockSignaturesWithAttestation to SignedBlock / BlockSignatures, but there is no migration or protocol/version gate. Existing on-disk blocks will now panic on expect("valid signatures") when read back, and mixed-version peers will silently drop each other’s blocks at decode time. If this rollout assumes a fresh DB and homogeneous network, that needs to be enforced or documented explicitly.

I did not run tests successfully because cargo/rustup is blocked in this sandbox by a read-only ~/.rustup temp path. Beyond the two points above, the split attestation/proposal key usage in the verification paths looks consistent.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Here is the code review for PR #283:


PR Overview

This PR migrates ethlambda to the devnet4 protocol by:

  • Introducing dual XMSS keys per validator (attestation_key + proposal_key) to prevent OTS reuse when a proposer signs both an attestation and a block in the same slot
  • Removing the BlockWithAttestation / proposer_attestation wrapper — the proposer now signs the block root directly with the proposal key
  • Allowing all validators (including proposer) to attest at interval 1
  • Switching to a new XMSS scheme (dim 46, 2536-byte sigs) and updating lean-multisig API
  • Fixing log_inv_rate from 1 → 2 for cross-client aggregation compatibility
  • Adding a MAX_ATTESTATIONS_DATA = 16 per-block cap with deduplication enforcement

The dual-key design and the block-root signing change are architecturally sound.


Critical Issues

aggregate_signatures swallows all error information (crates/common/crypto/src/lib.rs)

// New
let (_sorted_pubkeys, aggregate) = xmss_aggregate(&[], raw_xmss, &message.0, slot, 2);
let serialized = aggregate.serialize();

xmss_aggregate now returns a bare tuple, not a Result. If the function panics internally the node crashes without a recoverable error path. The leftover AggregationError::AggregationPanicked variant is defined but never constructed — it is dead code. If the upstream API is truly infallible, remove the variant and add a comment. If it can panic, wrap the call in std::panic::catch_unwind and return AggregationPanicked.


compact_attestations silently discards real cryptographic proof data (crates/blockchain/src/store.rs)

// When merging duplicates:
compacted_proofs.push(AggregatedSignatureProof::empty(merged_bits.clone()));

Any non-empty proof_data in the merged entries is thrown away. In the current block-building path this cannot trigger (build_block already deduplicates via processed_data_roots), making compact_attestations a dead-code safety belt in practice. However, if it ever fires on real proofs the block will fail signature verification on the receiver side. At minimum add a debug_assert!(items[idx].take()... .proof_data.is_empty()) guard to surface this if it occurs, or document explicitly that the function is only called on empty proofs.


Notable Concerns

leansig pinned to a branch, not a commit hash (Cargo.toml)

leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" }

Any push to devnet4 will silently change the dependency on cargo update. For a consensus client, reproducible builds are critical. Pin to a specific rev once the branch stabilises.


_pubkey_hex is parsed but never verified against the loaded secret key (bin/ethlambda/src/main.rs:252)

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 (crates/common/crypto/src/lib.rs)

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

sign_with_proposal_key has no metrics (crates/blockchain/src/key_manager.rs:100-128)

sign_with_attestation_key records timing and increments inc_pq_sig_attestation_signatures. sign_with_proposal_key has neither. Add equivalent counters so proposal signing is observable.


stf_spectests.rs justified slots comparison iterates fixture length, not state length

// 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 (post_state.justified_slots.len() > justified_slots.data.len()), extra bits set in the state are not checked. The direction should be the union, or the assertion should verify both lengths match before comparing.


on_block_rejects_duplicate_attestation_data test uses 0 validators (crates/blockchain/src/store.rs:1676)

The test creates State::from_genesis(1000, vec![]) and submits a block with proposer_index: 0. The proposer-validity check runs before the duplicate-attestation check. Calling is_proposer(0, 1, 0) with num_validators = 0 is undefined (modulo by zero). The test should either use a state with at least one validator, or place the duplicate-attestation check before the proposer check.


deser_pubkey_hex error message loses field context (crates/common/types/src/genesis.rs:32)

.map_err(|_| D::Error::custom(format!("pubkey is not valid hex: {s}")))?;

The old per-array-index message ("GENESIS_VALIDATORS[{idx}] is not valid hex") is gone. Serde adds struct field context at the outer level, so this is likely acceptable. Minor.


ValidatorKeyRole doesn't implement Display, used with {role:?} in error string (bin/ethlambda/src/main.rs:351)

format!("validator {}: duplicate {role:?} entry", entry.index) uses Debug formatting. The output (Attestation / Proposal) is readable, but implementing Display would produce cleaner error messages for operators.


Good Changes

  • read_validator_keys now returns Result<_, String> and propagates errors cleanly instead of panicking at multiple sites. The classify_role helper is clear and matches zeam/lantern semantics.
  • The compact_attestations function is well-tested (no-duplicates, merge, order-preservation cases) and the on_block_rejects_duplicate_attestation_data integration test covers the rejection path.
  • log_inv_rate = 2 fix is well-documented with a cross-client rationale comment. This is a real correctness fix.
  • Removing BlockWithAttestation and the proposer-attestation wrapper simplifies the type hierarchy significantly. The diff is mechanical and consistent across all layers.
  • Checkpoint sync verification correctly extended to check both attestation_pubkey and proposal_pubkey (bin/ethlambda/src/checkpoint_sync.rs:148).

Automated review by Claude (Anthropic) · sonnet · custom prompt

MegaRedHand and others added 4 commits April 14, 2026 17:58
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).
Co-authored-by: Pablo Deymonnaz <pdeymon@fi.uba.ar>
This PR adds true attestation aggregation on the proposer block building
pipeline. As part of this, the attestation byte cap we added was removed
since we now have the 16 attestation+proof cap.
@MegaRedHand MegaRedHand marked this pull request as ready for review April 15, 2026 19:03
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

This is a substantial PR implementing pq-devnet-4 specification changes, primarily the dual-XMSS-key architecture (separate attestation and proposal keys) and updated signature aggregation strategies.

Summary of Changes

Architecture:

  • Dual XMSS Keys: Each validator now has independent attestation_pubkey and proposal_pubkey (52 bytes each), allowing signing both attestations and block proposals within the same slot without OTS reuse.
  • Block Structure: Removed BlockWithAttestation wrapper; SignedBlock now contains message: Block directly. Proposer signatures are now over the block root (using proposal key) rather than an attestation.
  • Attestation Flow: All validators including proposers now produce attestations at interval 1.

Cryptography:

  • Updated leansig to devnet4 branch with aborting hypercube message hash (signature size reduced from 3112 to 2536 bytes).
  • Implemented mixed aggregation (aggregate_mixed) combining existing proofs (children) with raw signatures in single xmss_aggregate call.
  • Implemented recursive proof compaction (compact_attestations) merging multiple proofs for same AttestationData via aggregate_proofs.

Consensus:

  • Added MAX_ATTESTATIONS_DATA = 16 limit per block.
  • Added duplicate attestation data detection in on_block.
  • Updated fork choice to handle new payload buffer semantics.

Detailed Review

1. Key Management (crates/blockchain/src/key_manager.rs)

Lines 20-35, 83-159: Correctly implements dual-key architecture.

  • ValidatorKeyPair holds both keys independently.
  • sign_attestation uses attestation_key; sign_block_root uses proposal_key.
  • Both methods properly advance XMSS preparation windows independently.

Security Note: The XMSS exhaustion check (lines 95-105 for attestation, 136-146 for proposal) correctly detects when advance_preparation stalls (interval unchanged), preventing signature generation beyond key lifetime.

2. Block Production (crates/blockchain/src/lib.rs)

Lines 218-269: Correctly refactored block signing.

  • Removed proposer attestation construction from block body.
  • Proposer now signs block_root directly with proposal key (line 237-246).
  • Returns SignedBlock instead of SignedBlockWithAttestation.

Consensus Correctness: The removal of proposer_attestation from block body and signing block root directly aligns with devnet4 spec, preventing circular weight advantage in fork choice.

3. Signature Aggregation (crates/blockchain/src/store.rs)

Lines 154-422: Complex but correct mixed aggregation implementation.

aggregate_committee_signatures (lines 154-209):

  • Properly unions new_payload_keys with gossip_groups (handling both cases where attestation data exists in pending payloads or only in gossip).
  • Uses greedy set-cover for proof selection (lines 347-387).

try_aggregate (lines 270-320):

  • Correctly filters children with unresolvable pubkeys (lines 290-302).
  • Validates that sufficient inputs exist after filtering (lines 305-308).
  • Uses log_inv_rate=2 matching cross-client convention (comment line 101 in crypto/src/lib.rs).

compact_attestations (lines 425-496):

  • Correctly groups by AttestationData preserving first-occurrence order.
  • Uses aggregate_proofs for merging (requires ≥2 children, enforced at line 454).
  • Unions AggregationBits properly (lines 398-409).

Potential Issue: In compact_attestations, if group_items.len() >= 2 but some children fail pubkey resolution during the mapping at lines 460-472, the error propagates up and fails block building. Consider filtering failed resolutions instead of failing entirely, though this is conservative/safe behavior.

4. Block Validation (crates/blockchain/src/store.rs)

Lines 680-718: Duplicate attestation detection.

// Lines 696-702
if !seen.insert(&att.data) {
    return Err(StoreError::DuplicateAttestationData { ... });
}
if seen.len() > MAX_ATTESTATIONS_DATA {
    return Err(StoreError::TooManyAttestationData { ... });
}

Correct: Enforces devnet4 spec constraints (leanSpec #536).

Lines 1527-1547: Signature verification updated correctly.

  • Uses get_proposal_pubkey() for proposer signature verification (line 1540).
  • Verifies against block_root not attestation data (line 1544).

5. Genesis & State (crates/common/types/src/genesis.rs, state.rs)

Lines 13-22 (genesis.rs): Correctly parses new GenesisValidatorEntry with dual keys.

Lines 65-76 (state.rs): Validator struct updated with attestation_pubkey and proposal_pubkey.

State Root: Test at line 155 pins state root to babcdc9235a29dfc0d605961df51cfc85732f85291c2beea8b7510a92ec458fe - this will catch any accidental SSZ layout changes.

6. Storage (crates/storage/src/store.rs)

Lines 893-931: New methods proof_count_for_data, existing_proofs_for_data, new_payload_keys correctly expose payload buffer state for the aggregation algorithm without unnecessary cloning.

7. Dependencies (Cargo.toml, Cargo.lock)

Lines 58, 61 (Cargo.toml):

  • leansig updated to branch = "devnet4" (was rev 73bedc2).
  • rand updated to 0.10 (was 0.9).

Security: lean-multisig rev 2eb4b9d includes updated Plonky3 dependencies and new XMSS instantiation (aborting hypercube). Verify this commit is properly audited.


Minor Issues & Suggestions

Item 1: Error Message Clarity

File: crates/blockchain/src/store.rs, line 1124

#[error("Block contains {count} distinct AttestationData entries; maximum is {max}")]
TooManyAttestationData { count: usize, max: usize },

Suggestion: Include the slot number in this error for easier debugging.

Item 2: Test Fixture Path

File: crates/net/p2p/src/gossipsub/encoding.rs, line 60

let block_bytes = include_bytes!("../../test_data/signed_block.ssz");

Note: Test marked as ignore due to devnet3 fixture. Ensure devnet4 fixture is added in follow-up.

Item 3: Documentation Consistency

File: CLAUDE.md, line 106

pub fn insert_signed_block(&mut self, root: H256, signed_block: SignedBlock) { ... }

Note: Comment mentions SignedBlock but the parameter was SignedBlockWithAttestation in the diff context (line 106 shows the change). This is correct in the updated version.

Item 4: Rand Version Compatibility

File: Cargo.toml, line 61
rand = "0.10" is used alongside rand 0.8.5 (transitive). Verify that ValidatorSecretKey::from_bytes and XMSS operations are compatible with the new rand version (0.10 uses getrandom 0.2.17 vs 0.4.2 for 0.10, but the crypto crate uses its own RNG).


Conclusion

Approval: This PR correctly implements the pq-devnet-4 specification changes. The dual-key architecture is sound, the mixed aggregation logic is sophisticated but correct, and block validation properly enforces new constraints.

Action Items:

  1. Verify lean-multisig commit 2eb4b9d is from a trusted/audited branch.
  2. Add devnet4 SSZ test fixture for signed_block.ssz (currently ignored).
  3. Consider adding metrics for XMSS key exhaustion events (currently only logged).

The code is well-structured, properly commented regarding spec compliance (leanSpec PR references), and maintains safety invariants around signature verification and state transitions.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Now I have enough to write a thorough review.


Code Review: lambdaclass/ethlambda PR #283 — feat: add devnet4 support

Overview

This is a large, protocol-level upgrade upgrading from devnet3 to devnet4. The main changes are:

  • Dual XMSS key pairs per validator: separate attestation_key and proposal_key
  • Block structure simplification: SignedBlockWithAttestationSignedBlock (removes the proposer-attestation wrapper)
  • Proposer-attestation removal: proposers now sign the block root with their proposal key; all validators (including proposer) attest at interval 1
  • log_inv_rate fix: changed from 1 → 2 to match all other cross-client implementations
  • Mixed/recursive aggregation (aggregate_mixed, aggregate_proofs): incremental proof building
  • compact_attestations: deduplicates AttestationData entries per block
  • MAX_ATTESTATIONS_DATA = 16: replaces the old byte-budget cap
  • XMSS scheme change: Poseidon1 / aborting hypercube, dimension 46, signature size 3112 → 2536 bytes

The structural changes are clean, well-documented, and consistently applied across all crates. The log_inv_rate fix is critical for cross-client interoperability and is the most impactful correctness fix.


Critical / High Priority

1. Floating leansig branch dependency (Cargo.toml:6101)

leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" }

The old rev was pinned (73bedc26...). A floating branch means cargo update can silently pull in breaking changes to the post-quantum signing library. For a consensus-critical dependency this is a meaningful risk. Pin to a specific commit hash before merging.

2. Panic on slot overflow in aggregation path (store.rs, try_aggregate, compact_attestations)

let slot_u32: u32 = slot.try_into().expect("slot exceeds u32");

This appears in both try_aggregate and compact_attestations. At 4 s/slot, u32::MAX is ~544 years away, but an adversarial block with an absurdly large slot number would panic the aggregator at interval 2 and potentially crash the node. Consider mapping this to a StoreError variant or an early validation check that rejects slots > u32::MAX at block ingestion.

3. Four spec tests silently skipped (forkchoice_spectests.rs:8106-8111)

const SKIP_TESTS: &[&str] = &[
    "test_gossip_attestation_with_invalid_signature",
    "test_block_builder_fixed_point_advances_justification",
    "test_equivocating_proposer_with_split_attestations",
    "test_finalization_prunes_stale_aggregated_payloads",
];

test_gossip_attestation_with_invalid_signature is a security-relevant test — skipping it means there is no automated regression guard for invalid-signature acceptance. The comment says "gossip attestation not serialized in fixture" but the existing on_gossip_attestation_without_verification path bypasses signatures anyway, which may explain why the test fails. These need tracked issues before merge. A compile-time assertion with a counter or a #[ignore = "reason"] per test would be more visible than a runtime skip list.

4. gossipAggregatedAttestation silently defaults to validator_id = 0 (forkchoice_spectests.rs:8193)

let validator_id = att_data.validator_id.unwrap_or(0);

This routes aggregated attestations through the single-attestation no-verification path with an arbitrary validator ID. The inline comment acknowledges the bypass but the spec fixture may expect actual aggregation semantics with the real participant set. This means the skipped tests test_equivocating_proposer_with_split_attestations and test_finalization_prunes_stale_aggregated_payloads are likely failing precisely because this routing is wrong. Should be tracked alongside the above.

5. Loaded secret key not validated against pubkey_hex (main.rs:6264-6266)

/// Parsed for hex-format validation only; not cross-checked against the
/// loaded secret key since leansig doesn't expose any pk getters.
#[serde(rename = "pubkey_hex", deserialize_with = "deser_pubkey_hex")]
_pubkey_hex: ValidatorPubkeyBytes,

If the YAML pubkey_hex doesn't match the loaded private key file, the validator will silently use the wrong key. The PR comment acknowledges the limitation. This is an operational risk (mis-configured deployments sign with wrong key, leading to rejected attestations/blocks). Worth opening a tracking issue if leansig doesn't expose a way to derive the public key from the secret key.


Medium Priority

6. Sort non-determinism in build_block (store.rs:7580)

// Before
sorted_entries.sort_by_key(|(data_root, (data, _))| (data.target.slot, **data_root));
// After
sorted_entries.sort_by_key(|(_, (data, _))| data.target.slot);

Rust's sort_by_key is stable, so equal-key entries preserve insertion order from the payload HashMap. But HashMap iteration order is non-deterministic. Two nodes with the same attestation pool but different insertion histories could include different attestations when target.slot ties. The old secondary sort on data_root was deterministic. The comment "match the spec's processing order" implies the spec mandates single-key sort — if so that's fine, but if not, the secondary sort key should be restored to prevent subtle divergence.

7. Cross-client test vectors removed without replacement (crypto/src/lib.rs:8812-8881)

The ream cross-client XMSS compatibility tests are deleted (old 3112-byte format no longer valid). This is expected with the scheme change, but the PR leaves a gap: there are no devnet4-format cross-client vectors yet. These were the only automated interoperability checks for the cryptographic layer. Add new vectors (or mark the removal explicitly as a follow-up issue) to avoid regression.

8. PostBlockCheckpoints likely vestigial

let Ok((block, attestation_signatures, _post_checkpoints)) =
    store::produce_block_with_signatures(...)

The _ prefix suppresses the warning but PostBlockCheckpoints was used to construct the proposer attestation, which no longer exists. If nothing else uses it, the struct and the tuple return value can be simplified. Small cleanup, but dead code in consensus-path logic.

9. aggregate_mixed / aggregate_proofs: xmss_aggregate panics on invalid children

The doc comment on aggregate_mixed (crypto/src/lib.rs:8676) says:

Panics if any deserialized child proof is cryptographically invalid (e.g., was produced for a different message or slot). This is an upstream constraint of xmss_aggregate.

This means a corrupted proof in the payload buffer could bring down the aggregator. Since child proofs arrive via gossip, this is a potential DoS vector. Ideally the library should surface an error rather than panic; if that's not possible, wrapping the call in std::panic::catch_unwind would contain the blast radius.

10. to_children_refs clones AggregatedXMSS (crypto/src/lib.rs:8767-8773)

fn to_children_refs(
    deserialized: &[(Vec<LeanSigPubKey>, AggregatedXMSS)],
) -> Vec<(&[LeanSigPubKey], AggregatedXMSS)> {
    deserialized.iter().map(|(pks, agg)| (pks.as_slice(), agg.clone())).collect()
}

AggregatedXMSS is cloned here. If it contains large proof data (which it will for XMSS aggregated proofs), this is an allocation per child per aggregation round. The clone is forced by the xmss_aggregate API taking ownership. A note in the doc comment would help future maintainers understand why this is unavoidable.


Minor / Style

11. read_validator_keys uses String as error type (main.rs:6319)

) -> Result<HashMap<u64, ValidatorKeyPair>, String> {

Using String as an error type is a code smell; anyhow::Error (already in Cargo.toml) would give richer context and consistent error propagation. The call site .expect("Failed to load validator keys") discards all error detail anyway; a proper error chain would improve diagnostics.

12. validate_checks label resolution behavior change

The old code returned an error if a label was provided without a corresponding root; the new code silently returns None if the label isn't in block_registry. If a fixture test has a headRootLabel for a block that was never registered, the check is now silently skipped rather than failing loudly. This may hide test harness bugs.

13. select_proofs_greedily O(n²) complexity

The greedy loop scans all remaining proofs on every iteration: O(proof_count² × validators_per_proof). For a bounded set (MAX_ATTESTATIONS_DATA = 16) this is harmless, but worth noting if the cap increases.


What Looks Good

  • The classify_role filename-based key routing is clearly documented with references to zeam/lantern as consistency anchors.
  • Duplicate attestation data validation (DuplicateAttestationData, TooManyAttestationData errors) is correctly placed before signature verification, preventing wasted crypto work on malformed blocks.
  • debug_assert_eq! in compact_attestations is a nice correctness guard.
  • New test on_block_rejects_duplicate_attestation_data directly covers the new validation path.
  • union_aggregation_bits handles mismatched-length bitfields correctly via max_len.
  • deser_pubkey_hex error messages now correctly avoid including the full 104-char hex string in format strings that would truncate error context.
  • CLAUDE.md and architecture docs are updated in sync with code changes.

Automated review by Claude (Anthropic) · sonnet · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. Deterministic block construction regressed in store.rs#L1360. build_block now sorts candidate attestations only by target.slot, but aggregated_payloads is a HashMap, so entries with the same target slot keep hash iteration order. That makes attestation ordering in block.body.attestations non-deterministic across processes, which changes the block root and proposer signature for the same fork-choice view. The old (target.slot, data_root) tie-breaker avoided this.

  2. The old wire-size safety check was removed, but the replacement only caps the number of distinct attestation data entries. In store.rs#L1371 the builder stops at MAX_ATTESTATIONS_DATA == 16, while each proof is still allowed to be up to 1 MiB in block.rs#L73 and block.rs#L80. That means a valid local block can still exceed the 10 MiB gossip limit if proofs are larger than the “realistic” test fixture. This is a liveness / DoS regression for proposers.

  3. The new recursive aggregation path can combine overlapping child proofs without enforcing disjoint participant sets. select_proofs_greedily and extend_proofs_greedily accept any proof that adds some new coverage in store.rs#L354 and store.rs#L1271, but try_aggregate/compact_attestations later collapse participant metadata to a union in store.rs#L336 and store.rs#L1225. For overlapping children like {0,1} + {1,2}, validator 1 is fed into aggregation twice while the advertised participants bitfield only contains it once. Unless lean-multisig explicitly dedups overlapping children internally, this can generate proofs whose metadata no longer matches what verify_signatures checks.

The dual-key plumbing itself looks coherent otherwise: attestation verification now consistently uses attestation keys, proposer verification uses proposal keys, and checkpoint sync validates both key sets. I couldn’t run cargo test here because the sandbox blocks fetching the git dependencies.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 15, 2026

Greptile Summary

This PR upgrades ethlambda from devnet3 to devnet4, which involves a major protocol restructuring: (1) the SignedBlockWithAttestation wire type is simplified to SignedBlock (dropping the embedded proposer-attestation envelope), (2) each validator now holds two independent XMSS keypairs (attestation_pubkey / proposal_pubkey), (3) the signature scheme changes to SchemeAbortingTargetSumLifetime32Dim46Base8 (size drops 3112→2536 bytes), (4) block-size budget switches from a byte cap to a count-based cap (MAX_ATTESTATIONS_DATA = 16), and (5) the aggregation pipeline is upgraded to mixed/recursive proof aggregation (aggregate_mixed / aggregate_proofs). The proposer now attests at interval 1 like all other validators instead of embedding a separate attestation in the block envelope.

  • Four fork-choice spec tests are skipped with a TODO comment; notably test_block_builder_fixed_point_advances_justification and test_finalization_prunes_stale_aggregated_payloads cover correctness properties that are not yet exercised by the new harness.
  • The leansig workspace dependency is pinned to a branch (branch = "devnet4") rather than a fixed commit, making future cargo update builds floating for a cryptography-critical crate.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/quality suggestions with no blocking defects.

The PR makes a large but well-structured protocol upgrade. All logic changes are directly tied to the devnet4 spec. The sort change is spec-intentional, the skipped tests are acknowledged with TODO comments, the floating leansig ref is Cargo.lock-protected for current builds, and the validator_id defaulting is test-only. All remaining comments are P2.

Cargo.toml (floating leansig branch ref), crates/blockchain/tests/forkchoice_spectests.rs (skipped correctness tests, validator_id=0 default)

Important Files Changed

Filename Overview
crates/blockchain/src/store.rs Core fork-choice store rewritten for devnet4: replaces byte-budget cap with count-based cap (MAX_ATTESTATIONS_DATA=16), adds mixed/recursive aggregation, adds compact_attestations + select_proofs_greedily, and drops proposer-attestation handling. Non-deterministic sort tiebreaker removed.
crates/common/crypto/src/lib.rs Replaces devnet3 lean-multisig API with devnet4 API (AggregatedXMSS, xmss_aggregate, xmss_verify_aggregation). Adds aggregate_mixed and aggregate_proofs. Removes ream devnet3 cross-client compatibility test vectors without devnet4 replacements.
crates/common/types/src/block.rs SignedBlockWithAttestation → SignedBlock; removes BlockWithAttestation wrapper and BlockSignaturesWithAttestation; proposer_signature now covers block root instead of proposer attestation data.
crates/common/types/src/state.rs Validator struct split pubkey into attestation_pubkey + proposal_pubkey; added get_proposal_pubkey() alongside get_attestation_pubkey().
crates/blockchain/src/key_manager.rs KeyManager upgraded to ValidatorKeyPair (attestation_key + proposal_key); added sign_block_root using proposal key.
bin/ethlambda/src/main.rs read_validator_keys now returns Result and classifies keys as attester/proposer by filename; startup propagates error properly.
crates/blockchain/tests/forkchoice_spectests.rs Adds attestation/gossipAggregatedAttestation step handling; resolves block-root labels; adds SKIP_TESTS for 4 tests. gossipAggregatedAttestation defaults validator_id to 0 when absent.
Cargo.toml leansig bumped to branch "devnet4" (floating ref) instead of a pinned rev; rand bumped 0.9→0.10.

Sequence Diagram

sequenceDiagram
    participant V as Validator (Proposer)
    participant KM as KeyManager
    participant Store as BlockChain Store
    participant P2P as P2P Layer

    Note over V,P2P: Interval 0 — Block Proposal
    V->>Store: produce_block_with_signatures(slot, validator_id)
    Store-->>V: (Block, attestation_signatures[])
    V->>KM: sign_block_root(validator_id, slot, block_root) [proposal_key]
    KM-->>V: proposer_signature (XmssSignature, 2536 bytes)
    V->>Store: on_block — verify attestation sigs via attestation_pubkey, proposer sig via proposal_pubkey over block_root
    V->>P2P: publish_block(signed_block)

    Note over V,P2P: Interval 1 — All Validators Attest (including proposer)
    V->>Store: produce_attestation_data(slot)
    Store-->>V: AttestationData
    V->>KM: sign_attestation(validator_id, att_data) [attestation_key]
    KM-->>V: XmssSignature (2536 bytes)
    V->>P2P: publish gossip SignedAttestation

    Note over V,P2P: Interval 2 — Aggregation (if is_aggregator)
    V->>Store: aggregate_committee_signatures()
    Store-->>P2P: gossip SignedAggregatedAttestation[]

    Note over V,P2P: Interval 4 — Promote new to known payloads
    V->>Store: accept_new_attestations() fork choice update
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: Cargo.toml
Line: 57

Comment:
**Floating `leansig` branch reference**

`leansig` is pinned to a branch name rather than a fixed commit rev. Cargo.lock captures the current HEAD today, but any future `cargo update` will silently pull in new commits from `devnet4`, which is risky for a cryptography-critical crate. `lean-multisig` in the same PR is correctly pinned (`rev = "2eb4b9d"`); consider doing the same for `leansig` once the branch stabilises.

(Replace `branch = "devnet4"` with the commit hash from Cargo.lock once it's stable.)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: crates/blockchain/src/store.rs
Line: 1357

Comment:
**Non-deterministic sort tiebreaker removed**

Sorting only by `data.target.slot` (without `data_root` as a secondary key) makes block building non-deterministic when multiple entries share the same target slot. Because `aggregated_payloads` is a `HashMap`, its iteration order is randomized, and stable sort only preserves *that* arbitrary order. Two nodes with identical payload pools but different `HashMap` instances can select different attestation entries once the pool exceeds `MAX_ATTESTATIONS_DATA = 16`, which may affect multi-client interoperability.

The previous code used `(data.target.slot, **data_root)` as a fully deterministic key. If the spec intentionally omits a tiebreaker, it may be worth a brief comment noting that interop relies on all clients having the same pool state at build time.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: crates/blockchain/tests/forkchoice_spectests.rs
Line: 23-31

Comment:
**Correctness-critical spec tests skipped**

Two of the four skipped tests cover properties that are central to the devnet4 fork-choice: `test_block_builder_fixed_point_advances_justification` (verifies that the block builder runs until justification converges) and `test_finalization_prunes_stale_aggregated_payloads` (verifies that finalization clears the payload pool). Skipping them means these code paths are unexercised in CI. The comment attributes the failures to harness limitations (gossip attestations bypassing the new→known pipeline), so tracking these as follow-up issues would help ensure they're fixed before the devnet4 harness is complete.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: crates/blockchain/tests/forkchoice_spectests.rs
Line: 148-149

Comment:
**`gossipAggregatedAttestation` silently defaults `validator_id` to 0**

When the fixture omits `validatorId`, `att_data.validator_id.unwrap_or(0)` credits the vote to validator 0 regardless of which validator actually signed. This means fork-choice weight can be accumulated under the wrong validator, making the test results misleading for any fixture that relies on precise per-validator weighting. A guard that fails loudly when `validator_id` is absent would be safer:

```rust
let validator_id = att_data
    .validator_id
    .ok_or("gossipAggregatedAttestation step missing validator_id")?;
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: crates/common/crypto/src/lib.rs
Line: 409

Comment:
**Cross-client compatibility tests removed without devnet4 replacements**

The ream devnet3 test vectors were removed because they target the old signature scheme. The `SchemeAbortingTargetSumLifetime32Dim46Base8` scheme in devnet4 is fundamentally different (dim 46, aborting hypercube hash), so these vectors are intentionally invalid. Adding equivalent devnet4 test vectors produced by at least one other client (zeam, ream, or grandine) would restore cross-client confidence before the devnet goes live.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: add proposer proof aggregation (#2..." | Re-trigger Greptile

Comment thread Cargo.toml
@@ -55,7 +55,7 @@ prometheus = "0.14"
clap = { version = "4.3", features = ["derive", "env"] }

# XMSS signatures
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Floating leansig branch reference

leansig is pinned to a branch name rather than a fixed commit rev. Cargo.lock captures the current HEAD today, but any future cargo update will silently pull in new commits from devnet4, which is risky for a cryptography-critical crate. lean-multisig in the same PR is correctly pinned (rev = "2eb4b9d"); consider doing the same for leansig once the branch stabilises.

(Replace branch = "devnet4" with the commit hash from Cargo.lock once it's stable.)

Prompt To Fix With AI
This is a comment left during a code review.
Path: Cargo.toml
Line: 57

Comment:
**Floating `leansig` branch reference**

`leansig` is pinned to a branch name rather than a fixed commit rev. Cargo.lock captures the current HEAD today, but any future `cargo update` will silently pull in new commits from `devnet4`, which is risky for a cryptography-critical crate. `lean-multisig` in the same PR is correctly pinned (`rev = "2eb4b9d"`); consider doing the same for `leansig` once the branch stabilises.

(Replace `branch = "devnet4"` with the commit hash from Cargo.lock once it's stable.)

How can I resolve this? If you propose a fix, please make it concise.

@@ -1051,20 +1357,21 @@ fn build_block(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Non-deterministic sort tiebreaker removed

Sorting only by data.target.slot (without data_root as a secondary key) makes block building non-deterministic when multiple entries share the same target slot. Because aggregated_payloads is a HashMap, its iteration order is randomized, and stable sort only preserves that arbitrary order. Two nodes with identical payload pools but different HashMap instances can select different attestation entries once the pool exceeds MAX_ATTESTATIONS_DATA = 16, which may affect multi-client interoperability.

The previous code used (data.target.slot, **data_root) as a fully deterministic key. If the spec intentionally omits a tiebreaker, it may be worth a brief comment noting that interop relies on all clients having the same pool state at build time.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/src/store.rs
Line: 1357

Comment:
**Non-deterministic sort tiebreaker removed**

Sorting only by `data.target.slot` (without `data_root` as a secondary key) makes block building non-deterministic when multiple entries share the same target slot. Because `aggregated_payloads` is a `HashMap`, its iteration order is randomized, and stable sort only preserves *that* arbitrary order. Two nodes with identical payload pools but different `HashMap` instances can select different attestation entries once the pool exceeds `MAX_ATTESTATIONS_DATA = 16`, which may affect multi-client interoperability.

The previous code used `(data.target.slot, **data_root)` as a fully deterministic key. If the spec intentionally omits a tiebreaker, it may be worth a brief comment noting that interop relies on all clients having the same pool state at build time.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 23 to +31

// We don't check signatures in spec-tests, so invalid signature tests always pass.
// The gossipAggregatedAttestation/attestation tests fail because the harness inserts
// individual gossip attestations into known payloads (should be no-op) and aggregated
// attestations with validator_id=0 into known (should use proof.participants into new).
// TODO: fix these
const SKIP_TESTS: &[&str] = &[
"test_gossip_attestation_with_invalid_signature",
"test_block_builder_fixed_point_advances_justification",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Correctness-critical spec tests skipped

Two of the four skipped tests cover properties that are central to the devnet4 fork-choice: test_block_builder_fixed_point_advances_justification (verifies that the block builder runs until justification converges) and test_finalization_prunes_stale_aggregated_payloads (verifies that finalization clears the payload pool). Skipping them means these code paths are unexercised in CI. The comment attributes the failures to harness limitations (gossip attestations bypassing the new→known pipeline), so tracking these as follow-up issues would help ensure they're fixed before the devnet4 harness is complete.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/tests/forkchoice_spectests.rs
Line: 23-31

Comment:
**Correctness-critical spec tests skipped**

Two of the four skipped tests cover properties that are central to the devnet4 fork-choice: `test_block_builder_fixed_point_advances_justification` (verifies that the block builder runs until justification converges) and `test_finalization_prunes_stale_aggregated_payloads` (verifies that finalization clears the payload pool). Skipping them means these code paths are unexercised in CI. The comment attributes the failures to harness limitations (gossip attestations bypassing the new→known pipeline), so tracking these as follow-up issues would help ensure they're fixed before the devnet4 harness is complete.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +148 to +149
// (no aggregated proof or participant list), so we use the same
// non-verification path. This inserts directly into known payloads,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 gossipAggregatedAttestation silently defaults validator_id to 0

When the fixture omits validatorId, att_data.validator_id.unwrap_or(0) credits the vote to validator 0 regardless of which validator actually signed. This means fork-choice weight can be accumulated under the wrong validator, making the test results misleading for any fixture that relies on precise per-validator weighting. A guard that fails loudly when validator_id is absent would be safer:

let validator_id = att_data
    .validator_id
    .ok_or("gossipAggregatedAttestation step missing validator_id")?;
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/tests/forkchoice_spectests.rs
Line: 148-149

Comment:
**`gossipAggregatedAttestation` silently defaults `validator_id` to 0**

When the fixture omits `validatorId`, `att_data.validator_id.unwrap_or(0)` credits the vote to validator 0 regardless of which validator actually signed. This means fork-choice weight can be accumulated under the wrong validator, making the test results misleading for any fixture that relies on precise per-validator weighting. A guard that fails loudly when `validator_id` is absent would be safer:

```rust
let validator_id = att_data
    .validator_id
    .ok_or("gossipAggregatedAttestation step missing validator_id")?;
```

How can I resolve this? If you propose a fix, please make it concise.

@@ -305,73 +409,4 @@ mod tests {
"Verification should have failed with wrong slot"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Cross-client compatibility tests removed without devnet4 replacements

The ream devnet3 test vectors were removed because they target the old signature scheme. The SchemeAbortingTargetSumLifetime32Dim46Base8 scheme in devnet4 is fundamentally different (dim 46, aborting hypercube hash), so these vectors are intentionally invalid. Adding equivalent devnet4 test vectors produced by at least one other client (zeam, ream, or grandine) would restore cross-client confidence before the devnet goes live.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/crypto/src/lib.rs
Line: 409

Comment:
**Cross-client compatibility tests removed without devnet4 replacements**

The ream devnet3 test vectors were removed because they target the old signature scheme. The `SchemeAbortingTargetSumLifetime32Dim46Base8` scheme in devnet4 is fundamentally different (dim 46, aborting hypercube hash), so these vectors are intentionally invalid. Adding equivalent devnet4 test vectors produced by at least one other client (zeam, ream, or grandine) would restore cross-client confidence before the devnet goes live.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants