Add fork choice tree visualization#142
Add fork choice tree visualization#142pablodeymo wants to merge 3 commits intoadd-attestation-committee-count-clifrom
Conversation
Greptile SummaryAdds browser-based fork choice tree visualization with zero external dependencies. Refactors Key changes:
Issues already noted in previous threads:
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| crates/blockchain/fork_choice/src/lib.rs | Extracted compute_block_weights() from compute_lmd_ghost_head() with proper tests, clean refactor with no behavior change |
| crates/net/rpc/src/fork_choice.rs | New RPC handlers for fork choice JSON and HTML UI, includes proper tests, already noted duplicate attestation extraction logic |
| crates/net/rpc/static/fork_choice.html | 495-line D3.js visualization page, CDN without SRI already noted, otherwise clean implementation with proper error handling |
| crates/blockchain/src/store.rs | Removed local extract_attestations_from_aggregated_payloads(), now calls new storage layer methods |
| crates/storage/src/store.rs | Added extract_latest_attestations() and convenience wrapper to centralize attestation extraction logic |
Sequence Diagram
sequenceDiagram
participant Browser
participant RPC as RPC Server
participant Store as Store
participant FC as Fork Choice
Browser->>RPC: GET /lean/v0/fork_choice/ui
RPC-->>Browser: HTML page with D3.js
loop Every 2 seconds
Browser->>RPC: GET /lean/v0/fork_choice
RPC->>Store: get_live_chain()
Store-->>RPC: blocks HashMap
RPC->>Store: extract_latest_known_attestations()
Store-->>RPC: attestations HashMap
RPC->>Store: latest_justified(), latest_finalized()
Store-->>RPC: checkpoints
RPC->>FC: compute_block_weights(start_slot, blocks, attestations)
FC-->>RPC: weights HashMap
RPC->>Store: head(), safe_target(), head_state()
Store-->>RPC: head, safe_target, validator_count
RPC-->>Browser: JSON with nodes, head, justified, finalized
Browser->>Browser: Render tree with D3.js
end
Last reviewed commit: 41db6e0
crates/net/rpc/src/fork_choice.rs
Outdated
| fn extract_attestations(store: &Store) -> HashMap<u64, AttestationData> { | ||
| let mut result: HashMap<u64, AttestationData> = HashMap::new(); | ||
|
|
||
| for ((validator_id, data_root), _payload_list) in store.iter_known_aggregated_payloads() { | ||
| let Some(data) = store.get_attestation_data_by_root(&data_root) else { | ||
| continue; | ||
| }; | ||
|
|
||
| let should_update = result | ||
| .get(&validator_id) | ||
| .is_none_or(|existing| existing.slot < data.slot); | ||
|
|
||
| if should_update { | ||
| result.insert(validator_id, data); | ||
| } | ||
| } | ||
|
|
||
| result | ||
| } |
There was a problem hiding this comment.
duplicate logic with extract_attestations_from_aggregated_payloads in crates/blockchain/src/store.rs:109. consider extracting to shared module to avoid maintaining two copies
| fn extract_attestations(store: &Store) -> HashMap<u64, AttestationData> { | |
| let mut result: HashMap<u64, AttestationData> = HashMap::new(); | |
| for ((validator_id, data_root), _payload_list) in store.iter_known_aggregated_payloads() { | |
| let Some(data) = store.get_attestation_data_by_root(&data_root) else { | |
| continue; | |
| }; | |
| let should_update = result | |
| .get(&validator_id) | |
| .is_none_or(|existing| existing.slot < data.slot); | |
| if should_update { | |
| result.insert(validator_id, data); | |
| } | |
| } | |
| result | |
| } | |
| // Consider moving this to ethlambda-storage or a shared util module | |
| fn extract_attestations(store: &Store) -> HashMap<u64, AttestationData> { | |
| crate::blockchain::store::extract_attestations_from_aggregated_payloads( | |
| store, | |
| store.iter_known_aggregated_payloads(), | |
| ) | |
| } |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/src/fork_choice.rs
Line: 42-60
Comment:
duplicate logic with `extract_attestations_from_aggregated_payloads` in `crates/blockchain/src/store.rs:109`. consider extracting to shared module to avoid maintaining two copies
```suggestion
// Consider moving this to ethlambda-storage or a shared util module
fn extract_attestations(store: &Store) -> HashMap<u64, AttestationData> {
crate::blockchain::store::extract_attestations_from_aggregated_payloads(
store,
store.iter_known_aggregated_payloads(),
)
}
```
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>ethlambda Fork Choice</title> | ||
| <script src="https://d3js.org/d3.v7.min.js"></script> |
There was a problem hiding this comment.
loading D3.js from CDN without subresource integrity (SRI). if CDN is compromised, malicious code could be injected. consider adding SRI hash or bundling locally
| <script src="https://d3js.org/d3.v7.min.js"></script> | |
| <script src="https://d3js.org/d3.v7.min.js" integrity="sha384-..." crossorigin="anonymous"></script> |
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/static/fork_choice.html
Line: 7
Comment:
loading D3.js from CDN without subresource integrity (SRI). if CDN is compromised, malicious code could be injected. consider adding SRI hash or bundling locally
```suggestion
<script src="https://d3js.org/d3.v7.min.js" integrity="sha384-..." crossorigin="anonymous"></script>
```
How can I resolve this? If you propose a fix, please make it concise.|
@greptile-apps review the PR again |
…ng usage with local devnet and standalone nodes,
color coding, layout guide, and JSON API schema

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_choicereturns a JSON snapshot of the fork choice tree:{ "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/uiserves a self-contained HTML page that renders the fork tree in real time:weight / validator_countRefactoring
Extracted
compute_block_weights()fromcompute_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 existingcompute_lmd_ghost_headcallscompute_block_weightsinternally, so behavior is unchanged.Moved the
extract_attestationslogic from both the blockchain and RPC crates intoStore::extract_latest_attestations()andStore::extract_latest_known_attestations()to eliminate code duplication. The function naturally belongs onStoresince it only depends on Store methods.Documentation
Added
docs/fork_choice_visualization.mdwith 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
Or start a node manually:
2. Open the visualization
Navigate to
http://localhost:5054/lean/v0/fork_choice/uiin your browser.The page will start polling automatically and render the fork tree as blocks are produced.
3. Use the JSON API directly
Useful for scripting, monitoring, or building custom tooling on top of the fork choice data.
What to look for
Changes
crates/blockchain/fork_choice/src/lib.rscompute_block_weights(), refactorcompute_lmd_ghost_headto use it, add 2 unit testscrates/net/rpc/Cargo.tomlethlambda-fork-choicedependencycrates/net/rpc/src/lib.rsmod fork_choice, register 2 new routescrates/net/rpc/src/fork_choice.rscrates/net/rpc/static/fork_choice.htmlcrates/storage/src/store.rsextract_latest_attestations()andextract_latest_known_attestations()methodscrates/blockchain/src/store.rsextract_attestations_from_aggregated_payloads, use Store methodsdocs/fork_choice_visualization.mdCargo.lockTest plan
make fmt— cleanmake lint— no warningsmake test— all 84 tests passtest_compute_block_weights,test_compute_block_weights_emptytest_get_fork_choice_returns_json,test_get_fork_choice_ui_returns_html