Conversation
🤖 Kimi Code ReviewSecurity & Correctness Issues1. Critical: Slot/Interval Calculation Bug (
|
🤖 Claude Code ReviewBased on my comprehensive review of PR #107, here's my code review: PR #107 Review: Devnet 3 SupportOverviewThis PR implements significant protocol changes for devnet 3, including:
Critical Issues1. Time Conversion Bug (HIGH SEVERITY)Location: let interval = (time_since_genesis % SECONDS_PER_SLOT) * 1000 / MILLISECONDS_PER_INTERVAL;Problem: This calculation computes the interval from the timestamp's seconds component, but
Impact: Interval calculation will be incorrect for all non-second-aligned ticks, causing proposers/attesters to act at wrong intervals. Recommendation: Remove this calculation entirely. The let slot = store.time() / INTERVALS_PER_SLOT;
let interval = store.time() % INTERVALS_PER_SLOT;2. Non-Aggregator Nodes Miss Attestations (HIGH SEVERITY)Location: } else if is_aggregator {
let signature = ValidatorSignature::from_bytes(&signed_attestation.signature)
.map_err(|_| StoreError::SignatureDecodingFailed)?;
store.insert_gossip_signature(&attestation.data, validator_id, signature);
}Problem: Non-aggregator nodes only store attestation data by root but never insert into Impact: Non-aggregator validator nodes will make incorrect fork choice decisions due to missing attestation data. Recommendation: Always store in 3. Unused Variable (MEDIUM)Location: let _post_state = loop {Problem: Variable prefixed with Impact: Code won't compile. 4. Validator Index Out of Bounds Risk (MEDIUM SEVERITY)Location: let Ok(pubkey) = validators[*vid as usize].get_pubkey() else {
continue;
};Problem: No bounds check before indexing. If a malicious peer sends an attestation with an invalid validator ID and it gets stored in gossip_signatures, this will panic. Impact: Potential panic causing node crash. Recommendation: Add bounds check: if *vid as usize >= validators.len() {
continue;
}5. Signature Parsing Duplication (LOW SEVERITY)Location: The signature is already parsed and verified at lines 351-355. Parsing it again at 374 is redundant. Recommendation: Store the already-parsed signature instead of re-parsing. Design Concerns6. Aggregation ArchitectureThe two-table approach ( Observation: This is acceptable for devnet 3 with 7. Gossip Signature Deletion TimingLocation: Gossip signatures are deleted immediately after aggregation at interval 2. If aggregation fails partially (e.g., due to pubkey errors), those signatures are lost and won't be retried. Recommendation: Only delete signatures that were successfully aggregated, not all attempted ones. 8. Pruning EfficiencyLocation: Pruning functions iterate over entire tables, decode every entry, check slots, then batch delete. For large devnets, this could be expensive. Consideration: Not a bug, but monitor performance as network grows. Minor Issues9. Comment AccuracyLocation: // Update safe target slot metric (updated by store.on_tick at interval 3)Should be "interval 3" (correct in code, line 298-300). 10. Network Name HardcodedLocation: let network = "devnet3";Should be configurable or match genesis config network field. 11. Metrics Variable AssignmentLocation: The refactored code is cleaner but the variable name Security Analysis✅ Signature verification: Properly maintained in gossip path Testing Recommendations
Positive Aspects✅ Comprehensive refactoring to support committee subnets SummaryBlockers (must fix before merge):
Recommended fixes:
The architectural approach is sound, but the critical bugs need resolution before this can safely deploy to devnet 3. Automated review by Claude (Anthropic) · custom prompt |
🤖 Codex Code ReviewFindings (ordered by severity)
If you want, I can sanity-check the expected 800ms schedule and propose a concrete patch for the tick/time plumbing and aggregated attestation verification. Automated review by OpenAI Codex · custom prompt |
…ilding promote_new_aggregated_payloads was overwriting existing known entries instead of appending, causing loss of previously accumulated proofs. Block building was reading from the legacy AggregatedPayloads table which was never populated by the new gossip pipeline, so proposers couldn't use freshly aggregated proofs. Fix both by merging payload lists during promotion and switching block building to read from LatestKnownAggregatedPayloads. Remove the now- unused AggregatedPayloads table and all associated methods.
Resolve conflict: keep both is_aggregator field and pending_block_parents from main's storage refactor.
…ibility (#127) Change BlocksByRootRequest::from_ssz_bytes_compat to use SszDecode::from_ssz_bytes with the payload. We don't need to maintain compatibility with devnet-2 format, so we remove the previous serialization.
Move the seconds-to-milliseconds conversion from inside on_tick to its callers, so both BlockChainServer::on_tick and store::on_tick work natively with millisecond timestamps.
) ## Motivation During devnet-3 debugging we found that the network was producing blocks but never finalizing (`justified_slot=0`, `finalized_slot=0` after 450+ slots). The root cause: no node was started with `--is-aggregator`, so attestation signatures were silently dropped after verification (`store.rs:368`), resulting in all blocks having `attestation_count=0`. ## Description - **CLAUDE.md**: Add "Aggregator Flag Required for Finalization" to the Common Gotchas section, documenting the full attestation pipeline, root cause, and symptom - **README.md**: Add a note in the devnet section warning that at least one node must use `--is-aggregator` when running manually ## How to Test - Read the docs changes for accuracy - Verify by running a devnet without `--is-aggregator` (blocks produce, no finalization) vs. with it on at least one node (finalization advances)
## Motivation The leanSpec project updated its XMSS `Signature` class serialization in commit [`0340cc1`](leanEthereum/leanSpec#380) (part of [leanSpec#403](leanEthereum/leanSpec#403)). The `model_serializer` now emits signatures as hex-encoded SSZ bytes (`"0x..."`) instead of the previous structured JSON format with `path`, `rho`, and `hashes` sub-objects. When bumping `LEAN_SPEC_COMMIT_HASH` to `8b7636b` and regenerating fixtures, all 8 `verify_signatures` spec tests broke with deserialization errors because our test types (`ProposerSignature`, `SignaturePath`, `HashElement`, `RhoData`, `HashesData`) expected the old structured format. ## Changes **Bump leanSpec commit** (`ce8572e`): - Update `LEAN_SPEC_COMMIT_HASH` in Makefile to `8b7636b` **Fix signature fixture deserialization** (`1f69319`): - Add `deser_xmss_hex` helper (follows the existing `deser_pubkey_hex` pattern): strips `0x` prefix, hex-decodes to 3112 bytes, constructs `XmssSignature` - Change `TestSignatureBundle.proposer_signature` from `ProposerSignature` to `XmssSignature` with the new deserializer - Remove ~130 lines of dead code: `ProposerSignature` struct and its SSZ reconstruction logic, `SignaturePath`, `HashElement`, `RhoData`, `HashesData`, `SszXmssSignature`, `SszHashTreeOpening`, `HashDigest`, `Rho` type aliases, and their now-unused imports (`SszDecode`, `SszEncode`, `FixedVector`, `U28`, `U32`) Note: `attestationSignatures` format is unchanged. ## Test plan - [x] `make test` — all workspace tests pass - [x] 8/8 signature spec tests pass - [x] 14 STF spec tests pass - [x] 26 fork choice spec tests pass
## Motivation The leanSpec bump to `8b7636b` (#134) introduced several spec changes beyond the signature serialization fix. This PR ports three remaining behavioral changes to align ethlambda with the reference implementation: 1. **Stricter attestation validation** — leanSpec added two checks we were missing 2. **Safe target attestation pool merge** — leanSpec changed which attestations are visible during safe target computation 3. **Aggregated attestation broadcasting** — leanSpec now broadcasts aggregated attestations after aggregation ## Description ### 1. Stricter attestation validation (`validate_attestation_data`) Two new validation rules reject malformed attestations: - **Head >= target check**: Reject attestations where the head checkpoint slot is older than the target slot. Well-formed attestations already satisfy `head.slot >= target.slot`, but without this check we'd accept malformed ones. - **Head slot consistency check**: Reject attestations where the head checkpoint's `slot` field doesn't match the actual block's slot. This mirrors the existing checks for source and target checkpoints. New `StoreError` variants: `HeadOlderThanTarget`, `HeadSlotMismatch`. ### 2. Merge attestation pools in `update_safe_target` Previously, `update_safe_target` (called at interval 3) only looked at "new" attestation payloads. However, the migration from "new" to "known" happens at interval 4, and some attestations enter "known" directly without passing through "new": - Proposer's own attestation bundled in block body → lands in "known" directly via `on_block` - Node's own self-attestation → recorded locally in "known" Without merging both pools, these attestations were invisible to the safe target calculation, causing it to undercount support. Now both pools are merged before computing the safe target. No double-counting risk since `extract_attestations_from_aggregated_payloads` deduplicates by validator ID (latest slot wins). ### 3. Broadcast aggregated attestations after aggregation After aggregating committee signatures at interval 2, the node now broadcasts the resulting `SignedAggregatedAttestation` messages to the network. The `P2PMessage::PublishAggregatedAttestation` variant already existed but was never sent from the aggregation path. Changes: - `aggregate_committee_signatures` returns `Vec<SignedAggregatedAttestation>` - `store::on_tick` returns `Vec<SignedAggregatedAttestation>` - `BlockChainServer::on_tick` publishes each aggregate via the P2P channel Test call sites (`forkchoice_spectests`, `signature_spectests`) ignore the return value — `Vec<T>` is not `#[must_use]`, so this compiles without warnings. ## How to Test ```bash make fmt # Passes make lint # Passes make test # All 26 fork choice + 14 STF + 8 signature spec tests pass ``` Change 3 (broadcasting) is a runtime behavior validated in devnet testing. ## Related Issues Stacked on #134 (leanSpec bump to `8b7636b`). --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
## Motivation Devnet-3 runs with ethlambda and Zeam showed gossip messages not being delivered properly between clients, contributing to justification/finalization failures. ## Description - **Remove `validate_messages()` from gossipsub config** — was causing messages to be silently dropped. - **Remove `flood_publish(false)`** — was preventing reliable message propagation across clients. - **Change network identifier from `devnet3` to `devnet0`** — aligns with the network name used by other clients, ensuring gossipsub topic strings match (e.g., `/leanconsensus/devnet0/block/ssz_snappy`). ## How to test Full validation requires a multi-client devnet with Zeam nodes to verify gossip interop.
## Motivation `ATTESTATION_COMMITTEE_COUNT` was hardcoded to `1` in `crates/blockchain/src/lib.rs`, meaning all validators shared a single attestation subnet. Future devnets will use multiple committees (other clients like Zeam and Lantern already support this parameter), so the value needs to be configurable at runtime. ## Description Add a `--attestation-committee-count` CLI argument (default `1`) that controls how many attestation subnets exist. Validators are assigned to subnets via `validator_id % attestation_committee_count`. **Changes:** - **`bin/ethlambda/src/main.rs`**: Add `--attestation-committee-count` CLI argument with default value `1`, pass it to `start_p2p()` - **`crates/blockchain/src/lib.rs`**: Remove the `ATTESTATION_COMMITTEE_COUNT` constant (no longer needed since the value comes from CLI) - **`crates/net/p2p/src/lib.rs`**: Accept `attestation_committee_count` as a function parameter instead of importing the constant, use it for subnet ID calculation The hardcoded constant is removed entirely since clap provides the default. The blockchain crate doesn't store the value because it has no internal use for it — only the P2P layer needs it for subnet subscription. ## How to test - Run with explicit value: `--attestation-committee-count 1` (should behave identically to before) - Run without the flag (default `1`, same behavior) - `make lint` and `make test` pass with no warnings --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
# Conflicts: # crates/blockchain/tests/signature_types.rs
The previous path (../../../ethlambda/leanSpec/...) only resolved correctly from the canonical repo location. Using ../../leanSpec/... (relative to the package root) works in both the main repo and git worktrees.
## Motivation
Understanding the fork choice tree at runtime is essential for debugging
devnets and demonstrating how LMD GHOST works. Currently there is no way
to observe the tree, attestation weights, or checkpoint progression
without reading logs. This PR adds a browser-based real-time
visualization served from the existing RPC server, with zero new
external dependencies.
## Description
### New JSON API endpoint
`GET /lean/v0/fork_choice` returns a JSON snapshot of the fork choice
tree:
```json
{
"nodes": [
{
"root": "0xabcd1234...",
"slot": 42,
"parent_root": "0x5678efgh...",
"proposer_index": 3,
"weight": 5
}
],
"head": "0xabcd1234...",
"justified": { "root": "0x...", "slot": 10 },
"finalized": { "root": "0x...", "slot": 5 },
"safe_target": "0xdeadbeef...",
"validator_count": 8
}
```
### D3.js visualization page
`GET /lean/v0/fork_choice/ui` serves a self-contained HTML page that
renders the fork tree in real time:
- **Tree layout**: Y axis = slot (time flows downward), X axis = fork
spreading via D3 tree layout
- **Color-coded blocks**: green (finalized), blue (justified), yellow
(safe target), orange (head), gray (default)
- **Weight visualization**: circle radius scaled by `weight /
validator_count`
- **Info bar**: head slot, justified slot, finalized slot, validator
count
- **Tooltips**: hover any block to see root (truncated), slot, proposer,
weight
- **Auto-polling**: refreshes every 2 seconds
- **Auto-scroll**: keeps the head node visible
- **Dark theme**: monospace font, suitable for terminal-oriented
developers
### Refactoring
Extracted `compute_block_weights()` from `compute_lmd_ghost_head()` in
the fork choice crate. The weight computation logic was previously
inlined and discarded after head selection — now it's a public function
reusable by both fork choice and the RPC handler. The existing
`compute_lmd_ghost_head` calls `compute_block_weights` internally, so
behavior is unchanged.
Moved the `extract_attestations` logic from both the blockchain and RPC
crates into `Store::extract_latest_attestations()` and
`Store::extract_latest_known_attestations()` to eliminate code
duplication. The function naturally belongs on `Store` since it only
depends on Store methods.
### Documentation
Added `docs/fork_choice_visualization.md` with usage guide covering
endpoints, local devnet and standalone setup, color coding reference,
layout guide, and JSON API schema.
## How to Use
### 1. Start a local devnet
```bash
make run-devnet
```
Or start a node manually:
```bash
cargo run --release -- \
--custom-network-config-dir ./config \
--node-key ./keys/node.key \
--node-id 0 \
--metrics-port 5054
```
### 2. Open the visualization
Navigate to `http://localhost:5054/lean/v0/fork_choice/ui` in your
browser.
The page will start polling automatically and render the fork tree as
blocks are produced.
### 3. Use the JSON API directly
```bash
curl -s http://localhost:5054/lean/v0/fork_choice | jq .
```
Useful for scripting, monitoring, or building custom tooling on top of
the fork choice data.
### What to look for
- **Single chain**: all blocks in a vertical line — no forks occurring
- **Forks**: horizontal branching — competing chains with different
attestation weights
- **Color transitions**: watch blocks turn green as finalization
advances
- **Weight distribution**: larger circles = more validators attesting
through that block
## Changes
| File | Action | Description |
|------|--------|-------------|
| `crates/blockchain/fork_choice/src/lib.rs` | Modified | Extract
`compute_block_weights()`, refactor `compute_lmd_ghost_head` to use it,
add 2 unit tests |
| `crates/net/rpc/Cargo.toml` | Modified | Add `ethlambda-fork-choice`
dependency |
| `crates/net/rpc/src/lib.rs` | Modified | Add `mod fork_choice`,
register 2 new routes |
| `crates/net/rpc/src/fork_choice.rs` | Created | Response types, JSON
handler, HTML handler, 2 integration tests |
| `crates/net/rpc/static/fork_choice.html` | Created | D3.js v7
visualization (~495 lines, self-contained) |
| `crates/storage/src/store.rs` | Modified | Add
`extract_latest_attestations()` and
`extract_latest_known_attestations()` methods |
| `crates/blockchain/src/store.rs` | Modified | Remove duplicated
`extract_attestations_from_aggregated_payloads`, use Store methods |
| `docs/fork_choice_visualization.md` | Created | Usage documentation
for the visualization endpoints and JSON API |
| `Cargo.lock` | Modified | Updated lockfile |
## Test plan
- [x] `make fmt` — clean
- [x] `make lint` — no warnings
- [x] `make test` — all 84 tests pass
- [x] Fork choice spec tests (26 tests) — no regression from refactoring
- [x] New unit tests: `test_compute_block_weights`,
`test_compute_block_weights_empty`
- [x] New RPC tests: `test_get_fork_choice_returns_json`,
`test_get_fork_choice_ui_returns_html`
- [x] Manual: run local devnet, open visualization in browser
## Motivation This PR adds an ASCII tree directly to terminal logs so developers can monitor fork choice state without leaving the terminal. ## Description Adds a new `fork_choice_tree` module that renders the fork choice tree as ASCII art and logs it once per slot at interval 4 (end-of-slot attestation acceptance). ### Output format ``` Fork Choice Tree: Finalized: slot 38 | root 5678abcd Justified: slot 40 | root 1234efgh Head: slot 45 | root abcd1234 5678abcd(38)── 9abc0123(39)── 1234efgh(40)── a1b2c3d4(41) ─ 2 branches ├── dead0001(42)── beef0002(43)── abcd1234(44) * [w:5] └── fade0003(42)── [ ]── cafe0004(44) [w:2] ``` ### Conventions - `root(slot)` format using ShortRoot (8 hex chars) - `──` connectors for linear chains - `├──` / `└──` Unicode tree characters for forks - `*` marker on the head block - `[w:N]` weight annotation on leaf nodes - `[ ]` for single missing slot, `[N]` for N consecutive missing slots - Branches sorted by weight (heaviest first), tiebreaker on root hash - Depth truncation with `...` when chain exceeds 20 nodes ### Changes | File | Action | Description | |------|--------|-------------| | `crates/blockchain/src/fork_choice_tree.rs` | **New** | `format_fork_choice_tree()` pure formatter + `TreeRenderer` struct for recursive rendering | | `crates/blockchain/src/lib.rs` | Modified | Add `pub(crate) mod fork_choice_tree` declaration | | `crates/blockchain/src/store.rs` | Modified | Add `log_tree` parameter to `update_head`/`accept_new_attestations`; log tree at interval 4 only | ### Logging frequency To avoid excessive output, the tree is only printed at **interval 4** (end-of-slot), giving one tree per slot. Block imports (interval 0) and `on_block` calls pass `log_tree: false`. ### No new dependencies The formatter uses only `std::fmt::Write` and types already available in the blockchain crate (`H256`, `ShortRoot`, `Checkpoint`). ## How to Test ```bash make fmt # Passes make lint # Passes (0 warnings) make test # All tests pass (including 9 new unit tests for the formatter) ``` ### Unit tests - Linear chain renders correctly with all nodes in sequence - Fork with two branches shows `├──` / `└──` connectors and branch count - Single missing slot shows `[ ]` indicator - Multiple missing slots show `[N]` indicator - Empty blocks map returns minimal `(empty)` output - Head marker `*` appears on the correct node - Single-block chain (genesis) renders correctly - Depth truncation triggers `...` at MAX_DISPLAY_DEPTH (20) - Nested forks (sub-fork within a branch) render both levels of branching
Resolve conflicts: - store.rs: merge both import additions (AggregationBits from main, SignedAggregatedAttestation from devnet-3) - block.rs: keep participant_indices() method, drop redundant pub field getters removed in main
## Summary - Non-aggregator nodes no longer subscribe to attestation subnet topics, reducing unnecessary gossip traffic - Aggregator nodes retain current behavior (subscribe and collect raw attestations) - Non-aggregators can still publish their own attestations via gossipsub's fanout mechanism ## Changes | File | Change | |------|--------| | `bin/ethlambda/src/main.rs` | Pass `is_aggregator` flag to `start_p2p()` | | `crates/net/p2p/src/lib.rs` | Add `is_aggregator` param; wrap attestation `.subscribe()` in conditional | ## How it works Gossipsub's fanout mechanism allows publishing to topics without subscribing. When a node publishes to an unsubscribed topic, libp2p maintains a temporary fanout set of peers that *are* subscribed, forwarding the message through them. This means non-aggregators only need outbound delivery (publishing), not inbound collection (subscribing). ## Test plan - [x] `cargo fmt --all -- --check` - [x] `cargo clippy --workspace -- -D warnings` - [x] `cargo test --workspace --release` (all unit tests pass; spec tests require fixtures not in worktree) - [ ] Multi-client devnet: verify aggregator nodes still collect attestations and finalize; non-aggregators publish attestations but don't receive them Closes #159 --------- Co-authored-by: Pablo Deymonnaz <pdeymon@fi.uba.ar>
Greptile SummaryThis PR implements a major protocol upgrade from pq-devnet-2 to pq-devnet-3, introducing a new 5-interval timing model (5 × 800ms = 4s slots) and replacing the per-validator attestation architecture with an aggregated payload pipeline. Key architectural changes:
Implementation quality:
Issues found:
The changes are comprehensive and architecturally sound for a devnet upgrade, with proper separation of concerns between aggregators and non-aggregators. Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| crates/blockchain/src/lib.rs | Updated timing model to 5 intervals of 800ms, added aggregated attestation handling, and refactored interval-based tick scheduling |
| crates/blockchain/src/store.rs | Major refactor replacing per-validator attestation maps with aggregated payload pipeline, added committee signature aggregation at interval 2 |
| crates/storage/src/store.rs | Replaced attestation storage tables with aggregated payload tables, added attestation-data-by-root content-addressed storage |
| crates/storage/src/api/tables.rs | Updated table definitions removing old attestation tables, adding new aggregated payload tables and attestation data by root |
| crates/net/p2p/src/lib.rs | Added per-committee attestation subnets, aggregation topic, and conditional subscription logic for aggregators vs non-aggregators, but has inconsistent error handling |
| crates/net/p2p/src/gossipsub/handler.rs | Added aggregation topic handler and updated attestation handling to support subnet-based routing |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Gossip: Individual Attestation] -->|Interval 1| B{Is Aggregator?}
B -->|Yes| C[Store in GossipSignatures]
B -->|No| D[Discard - Wait for Aggregated]
C -->|Interval 2| E[aggregate_committee_signatures]
E --> F[Create SignedAggregatedAttestation]
F --> G[Publish to Aggregation Topic]
G --> H[All Nodes Receive]
H --> I[Store in LatestNewAggregatedPayloads]
I -->|Interval 0 with proposal| J[promote_new_aggregated_payloads]
I -->|Interval 4| J
J --> K[LatestKnownAggregatedPayloads]
K --> L[extract_latest_known_attestations]
L --> M[Fork Choice: compute_lmd_ghost_head]
N[Block Building] --> O[select_aggregated_proofs]
K --> O
O --> P[Block with Aggregated Attestations]
Last reviewed commit: 5918e8b
We weren't actually using the attestation processing without verification function, so we deleted it. We also aren't expecting any unaggregated attestations from gossip unless we are an aggregator, so having an is_aggregator parameter is not needed
…tations, and consolidate test utilities (#172) ## Summary Code quality cleanup after the devnet 3 merge (#107). Ran systematic reviews (code reuse, quality, efficiency) across all changed files and fixed the actionable findings. Net result: no behavior changes, reduced I/O round-trips on hot paths. - Deduplicate copy-pasted storage methods for aggregated payloads - Eliminate 4 redundant `tree_hash_root()` computations on hot paths - Remove duplicate test helpers in favor of existing public APIs - Add a `data_root` cache to avoid N+1 DB lookups in attestation extraction - Single-pass over known aggregated payloads during block production (was scanning the table twice) - Move `Checkpoint` to its own module with direct imports (no re-exports) - Batch aggregated payload inserts (single commit instead of N round-trips per validator) - Deduplicate prune functions via shared `prune_by_slot` helper - Narrow `extract_latest_attestations` API to accept only keys (avoids cloning large proof data) - Extract ~250 lines of duplicated test fixture types into shared `ethlambda-test-fixtures` crate ## Changes in detail ### 1. Storage method deduplication (`crates/storage/src/store.rs`) `iter_known_aggregated_payloads` / `iter_new_aggregated_payloads` and `insert_known_aggregated_payload` / `insert_new_aggregated_payload` were character-for-character identical except for the `Table` enum variant. **Fix:** Extracted two private helpers parameterized by `Table`: - `iter_aggregated_payloads(table)` — shared iterator logic - `insert_aggregated_payload(table, key, payload)` — shared read-modify-write logic ### 2. Redundant `tree_hash_root()` elimination (`crates/blockchain/src/store.rs`) Four call sites were computing `tree_hash_root()` on data that had already been hashed: | Location | What was redundant | |----------|-------------------| | `on_gossip_aggregated_attestation` | `aggregated.data.tree_hash_root()` computed twice | | `on_block_core` | `block.tree_hash_root()` computed before verification, then recomputed on the clone after | | `aggregate_committee_signatures` | `data.tree_hash_root()` recomputed on data already fetched by its `data_root` key | | `on_gossip_attestation` → `insert_gossip_signature` | `attestation.data.tree_hash_root()` computed in caller, then recomputed inside | **Fix:** Reuse the pre-computed value in each case. For `insert_gossip_signature`, changed the signature to accept `(data_root, slot)` instead of `&AttestationData`. ### 3. Test helper deduplication **`forkchoice_spectests.rs`:** Removed 20-line duplicate `extract_attestations()` — calls the public `Store::extract_latest_attestations()` directly. **`rpc/fork_choice.rs` + `rpc/lib.rs`:** Extracted shared `test_utils::create_test_state()`. ### 4. `data_root` cache in `extract_latest_attestations` (`crates/storage/src/store.rs`) Many validators attest to the same data (often just 1-3 distinct roots). Added a `HashMap<H256, Option<AttestationData>>` cache to deduplicate `get_attestation_data_by_root` lookups. ### 5. Single-pass in `produce_block_with_signatures` (`crates/blockchain/src/store.rs`) Was calling `store.iter_known_aggregated_payloads()` twice — once for attestation data, then again for proofs. Now collects once and derives both from it. ### 6. Move `Checkpoint` to dedicated module (`crates/common/types/src/checkpoint.rs`) Extracted `Checkpoint` struct and `deser_dec_str` helper from `state.rs`. All import sites updated to use `checkpoint::Checkpoint` directly — no `pub use` re-exports. ### 7. Batch aggregated payload inserts (`crates/storage/src/store.rs`) Each `insert_aggregated_payload` call performed its own read-modify-write-commit cycle. When storing an aggregated attestation covering N validators, this meant N separate storage round-trips. **Fix:** New `insert_aggregated_payloads_batch` performs a single read-write-commit cycle for all entries. Groups by key to handle duplicate keys correctly. Updated all loop callers in `aggregate_committee_signatures`, `on_gossip_aggregated_attestation`, and `on_block_core`. ### 8. Deduplicate prune functions (`crates/storage/src/store.rs`) `prune_gossip_signatures` and `prune_attestation_data_by_root` were structurally identical (iterate table, decode, check slot, collect keys, batch delete). Extracted shared `prune_by_slot(table, finalized_slot, get_slot)` helper parameterized by a slot-extraction closure. ### 9. Narrow `extract_latest_attestations` API Changed from `impl Iterator<Item = (SignatureKey, Vec<StoredAggregatedPayload>)>` to `impl Iterator<Item = SignatureKey>`. The function only uses the keys — passing full payloads forced `produce_block_with_signatures` to clone all `Vec<StoredAggregatedPayload>` (containing multi-KB XMSS proof data) unnecessarily. ### 10. Extract shared test fixture types into `ethlambda-test-fixtures` crate `blockchain/tests/common.rs` and `state_transition/tests/types.rs` contained ~250 lines of identical serde `Deserialize` structs and `From` impls for converting JSON test fixtures into domain types. Since these are integration tests in different crates, they couldn't share code via `mod`. **Fix:** New `crates/common/test-fixtures/` crate holds the 12 shared types (`Container`, `Config`, `Checkpoint`, `BlockHeader`, `Validator`, `TestState`, `Block`, `BlockBody`, `AggregatedAttestation`, `AggregationBits`, `AttestationData`, `TestInfo`) plus the `deser_pubkey_hex` helper. Both consumer files now re-export from the crate and keep only their test-specific types (`ProposerAttestation` in forkchoice, `StateTransitionTestVector`/`PostState` in STF). ### 11. Stale comment fix (`crates/blockchain/src/lib.rs`) Comment said "intervals 0/3" but the 5-interval model promotes attestations at intervals 0 and 4. ## How to test All existing tests cover these changes — no new behavior was introduced. ```bash cargo test --workspace --release cargo clippy --workspace -- -D warnings cargo fmt --all -- --check ``` All 102 tests pass.
…unify validator_indices, and clean up time constants (#174) ## Summary Follow-up cleanup from reviewing the devnet 3 merge (#107). Four targeted optimizations and deduplication fixes — no behavior changes. - Skip payload deserialization in `update_safe_target` (runs every slot) - Batch `insert_attestation_data_by_root` commits in `on_block_core` (runs every block) - Unify duplicated `aggregation_bits_to_validator_indices` into shared `validator_indices()` - Derive `MILLISECONDS_PER_SLOT` from base constants, remove redundant `SECONDS_PER_SLOT` ## Changes in detail ### 1. Key-only iterators for `update_safe_target` (`crates/storage/src/store.rs`, `crates/blockchain/src/store.rs`) `update_safe_target` calls `iter_known_aggregated_payloads()` and `iter_new_aggregated_payloads()`, which deserialize `Vec<StoredAggregatedPayload>` per entry (each containing multi-KB `AggregatedSignatureProof` objects). It then immediately discards the values and passes only the keys to `extract_latest_attestations`. **Fix:** Added `iter_known_aggregated_payload_keys()` and `iter_new_aggregated_payload_keys()` backed by a shared `iter_aggregated_payload_keys(table)` helper that decodes only the key bytes, skipping value deserialization entirely. `update_safe_target` now uses these through a `HashSet` for deduplication. This runs every slot (every 4 seconds) and scales with validator count. ### 2. Batch attestation data inserts in `on_block_core` (`crates/storage/src/store.rs`, `crates/blockchain/src/store.rs`) `on_block_core` called `insert_attestation_data_by_root` once per block body attestation plus once for the proposer attestation. Each call opened a separate write batch and committed — N+1 round-trips per block. **Fix:** Added `insert_attestation_data_by_root_batch` that writes all entries in a single batch-commit. `on_block_core` now collects all attestation data entries (body + proposer) and inserts them in one go. The existing `insert_attestation_data_by_root` is kept for the single-entry callers (`on_gossip_attestation`, `on_gossip_aggregated_attestation`). ### 3. Unify `aggregation_bits_to_validator_indices` (`crates/common/types/src/attestation.rs`, `crates/common/types/src/block.rs`, `crates/blockchain/src/store.rs`) `store.rs` had a free function `aggregation_bits_to_validator_indices(&AggregationBits) -> Vec<u64>` that was structurally identical to `AggregatedSignatureProof::participant_indices()` in `block.rs`. Both iterate a bitfield and collect set-bit indices. **Fix:** Added `validator_indices(&AggregationBits) -> impl Iterator<Item = u64>` in `ethlambda_types::attestation` (where `AggregationBits` is defined). `participant_indices()` now delegates to it. The local function in `store.rs` is removed; all 5 call sites use the shared function. ### 4. Derive `MILLISECONDS_PER_SLOT` and remove `SECONDS_PER_SLOT` (`crates/blockchain/src/lib.rs`, test files) Three independent time constants were defined: ```rust pub const SECONDS_PER_SLOT: u64 = 4; pub const MILLISECONDS_PER_SLOT: u64 = 4_000; pub const MILLISECONDS_PER_INTERVAL: u64 = 800; pub const INTERVALS_PER_SLOT: u64 = 5; ``` `SECONDS_PER_SLOT` was only used in two test files and was redundant. `MILLISECONDS_PER_SLOT` was independently defined rather than derived, creating a consistency risk if either base constant changes. **Fix:** Removed `SECONDS_PER_SLOT`. Made `MILLISECONDS_PER_SLOT = MILLISECONDS_PER_INTERVAL * INTERVALS_PER_SLOT`. Updated test callers (`forkchoice_spectests.rs`, `signature_spectests.rs`) to use `genesis_time * 1000 + slot * MILLISECONDS_PER_SLOT`, matching the production arithmetic in `get_proposal_head`. ## How to test All existing tests cover these changes — no new behavior was introduced. ```bash cargo test --workspace --release cargo clippy --workspace --tests -- -D warnings cargo fmt --all -- --check ``` All 102 tests pass.
Closes #73
Summary
Upgrades ethlambda from pq-devnet-2 to pq-devnet-3, implementing the new 5-interval timing model, committee aggregation pipeline, and networking topology.
Timing model (5 intervals × 800ms = 4s slots)
The slot is now divided into 5 intervals of 800ms each (previously 4 × 1s):
Aggregated attestation pipeline
Replaced the per-validator attestation maps (
LatestNewAttestations/LatestKnownAttestations) with an aggregated payload pipeline:For
skip-signature-verificationmode (tests), individual attestations bypass aggregation and insert directly intoLatestNewAggregatedPayloadswith dummy proofs.Networking
/leanconsensus/devnet3/attestation_{subnet_id}/ssz_snappy/leanconsensus/devnet3/aggregation/ssz_snappydevnet0→devnet3SignedAggregatedAttestationtype for aggregation gossip--is-aggregatorCLI flagBlock building
select_aggregated_proofs()(renamed fromcompute_aggregated_signatures) only selects existing proofs from theAggregatedPayloadstable — no more inline gossip signature collectionbuild_block()sources attestations fromLatestKnownAggregatedPayloadsproduce_block_with_signatures()uses the same aggregated payload sourceFork choice
update_head()andupdate_safe_target()reconstruct per-validator votes from aggregated payloads viaextract_attestations_from_aggregated_payloads()on_gossip_aggregated_attestation()handles aggregated attestations from the network, expanding per-participant into payload entriesTest fixtures
Updated
LEAN_SPEC_COMMIT_HASHtob39472e(leanSpec devnet-3 withINTERVALS_PER_SLOT=5). Regenerated all fixtures.