Skip to content

Comments

Add fork choice tree visualization#142

Open
pablodeymo wants to merge 3 commits intoadd-attestation-committee-count-clifrom
add-fork-choice-visualization
Open

Add fork choice tree visualization#142
pablodeymo wants to merge 3 commits intoadd-attestation-committee-count-clifrom
add-fork-choice-visualization

Conversation

@pablodeymo
Copy link
Collaborator

@pablodeymo pablodeymo commented Feb 23, 2026

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:

{
  "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

make run-devnet

Or start a node manually:

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

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

  • make fmt — clean
  • make lint — no warnings
  • make test — all 84 tests pass
  • Fork choice spec tests (26 tests) — no regression from refactoring
  • New unit tests: test_compute_block_weights, test_compute_block_weights_empty
  • New RPC tests: test_get_fork_choice_returns_json, test_get_fork_choice_ui_returns_html
  • Manual: run local devnet, open visualization in browser

@pablodeymo pablodeymo changed the base branch from main to add-attestation-committee-count-cli February 23, 2026 20:31
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 23, 2026

Greptile Summary

Adds browser-based fork choice tree visualization with zero external dependencies. Refactors compute_block_weights() out of compute_lmd_ghost_head() to enable reuse by the new JSON API endpoint. The attestation extraction logic was moved from blockchain/store.rs to storage/store.rs to centralize it.

Key changes:

  • New GET /lean/v0/fork_choice JSON API returns nodes, weights, head, justified/finalized checkpoints
  • New GET /lean/v0/fork_choice/ui serves self-contained HTML with D3.js visualization (auto-polling every 2s)
  • Clean refactor: weight computation now public and tested separately
  • Code deduplication: attestation extraction moved to storage layer

Issues already noted in previous threads:

  • Duplicate attestation extraction logic (noted at fork_choice.rs:42)
  • Missing SRI for D3.js CDN (noted in fork_choice.html)

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk once the noted attestation extraction duplication is addressed
  • Score reflects clean refactoring with proper tests, well-structured new endpoints, and comprehensive testing. The weight computation extraction is behavior-preserving and properly tested. The two previously noted issues (duplicate attestation extraction and missing SRI) are not critical blockers but should be addressed for code quality and security best practices.
  • Pay attention to crates/net/rpc/src/fork_choice.rs for the duplicate attestation extraction logic that could be refactored

Important Files Changed

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
Loading

Last reviewed commit: 41db6e0

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

27 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 42 to 60
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
}
Copy link
Contributor

Choose a reason for hiding this comment

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

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

Suggested change
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>
Copy link
Contributor

Choose a reason for hiding this comment

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

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

Suggested change
<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.

@pablodeymo
Copy link
Collaborator Author

Example of visualization:

Captura de pantalla 2026-02-23 a la(s) 5 45 40 p  m

@pablodeymo
Copy link
Collaborator Author

@greptile-apps review the PR again

…ng usage with local devnet and standalone nodes,

      color coding, layout guide, and JSON API schema
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.

1 participant