Skip to content

fix(forkchoice-viz): always render finalized block as fully filled#339

Open
conache wants to merge 2 commits intolambdaclass:mainfrom
conache:forkchoice-viz-fill-finalized-block-node
Open

fix(forkchoice-viz): always render finalized block as fully filled#339
conache wants to merge 2 commits intolambdaclass:mainfrom
conache:forkchoice-viz-fill-finalized-block-node

Conversation

@conache
Copy link
Copy Markdown
Contributor

@conache conache commented May 1, 2026

🗒️ Description / Motivation

The fork choice visualization currently renders the finalized block as empty. This happens because each node is filled proportionally to its weight value. This value is computed by the compute_block_weights() method, which returns 0 for the start_slot (which is always the finalized block's slot).

The reported 0 is technically accurate but misleading: a finalized block has full validator support by definition (2/3 supermajority and unbroken justified chain). However, this is only misleading for the visualisation, since compute_block_weights() correctly skips the finalized block because LMD GHOST treats it as the root of the fork-choice tree, not a candidate, so weighing it adds no information to head selection.

Proposed solution: render finalized blocks as fully filled green discs and show status: finalized in the tooltip instead of weight: 0.

Screenshot 2026-05-01 at 14 31 15

What Changed

  • crates/net/rpc/static/fork_choice.html:
  • nodeRatio: returns 1 for blocks at or below finalized.slot, so they render as solid green discs.
  • Tooltip shows status: finalized for finalized blocks instead of weight: 0.

Correctness / Behavior Guarantees

  • Only the forkchoice visualization changes; consensus and weight computation are unchanged.
  • Non-finalized blocks still fill proportionally to weight / validator_count.
  • Finalized blocks show up as fully filled. When hovering on a finalized block, there's a status field associated showing: status: finalized.

Tests Run

  • Manual: ran a single-node devnet and confirmed finalized blocks render as solid green discs with status: finalized in the tooltip.

Related Issues / PRs

@conache conache changed the title Completely fill the finalized block node in the forkchoice visualization fix(forkchoice-viz): always render finalized block as fully filled May 1, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR updates the fork-choice visualization so that finalized blocks render as fully filled green discs and display status: finalized in their tooltip, instead of showing weight: 0. All changes are confined to fork_choice.html; consensus and weight-computation logic are untouched.

  • _stroke computed but unused: nodeStroke and the _stroke property are introduced, but the .node-outer circle still reads d._color for its stroke on both enter and update — the intended darker outline never renders.
  • Tooltip regression: non-finalized blocks silently lose the weight/total (pct%) breakdown that was present before this PR.

Confidence Score: 3/5

Safe to merge with reservations — a newly introduced visual feature (_stroke darker outline) is silently inert, and non-finalized tooltip information is regressed.

One P1 (dead _stroke feature that never applies) and two P2s (tooltip regression, redundant guard) pull the score below the P1 ceiling of 4.

crates/net/rpc/static/fork_choice.html — specifically the _stroke wiring in the .node-outer enter/update blocks and the tooltip weight percentage.

Important Files Changed

Filename Overview
crates/net/rpc/static/fork_choice.html Visualization-only change: finalized blocks now render as fully filled green discs with "status: finalized" tooltip; introduces _stroke (computed but never applied to the outer circle), drops weight percentage from non-finalized tooltips, and contains a redundant guard in nodeRatio.

Comments Outside Diff (1)

  1. crates/net/rpc/static/fork_choice.html, line 229-230 (link)

    P2 Redundant guard in nodeRatio

    !validatorCount already returns true when validatorCount is 0 (since !0 === true), so the || validatorCount === 0 branch is unreachable dead code.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/net/rpc/static/fork_choice.html
    Line: 229-230
    
    Comment:
    **Redundant guard in `nodeRatio`**
    
    `!validatorCount` already returns `true` when `validatorCount` is `0` (since `!0 === true`), so the `|| validatorCount === 0` branch is unreachable dead code.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
crates/net/rpc/static/fork_choice.html:322
**`_stroke` is computed but never applied**

`_stroke` is stored on each flat node but the render code references `d._color` for both the initial enter and the transition update of `.node-outer`. The `nodeStroke` function (and this property) are therefore dead code — the darker outline never actually appears.

### Issue 2 of 3
crates/net/rpc/static/fork_choice.html:229-230
**Redundant guard in `nodeRatio`**

`!validatorCount` already returns `true` when `validatorCount` is `0` (since `!0 === true`), so the `|| validatorCount === 0` branch is unreachable dead code.

```suggestion
      if (!validatorCount) return 0;
```

### Issue 3 of 3
crates/net/rpc/static/fork_choice.html:350-356
**Non-finalized tooltip silently drops the weight percentage**

The old tooltip rendered `weight: X/total (pct%)` for non-finalized blocks; the new version shows only `weight: X`. The `total` and percentage were useful for understanding a node's relative support — this is a quiet regression for all non-finalized nodes in the tree.

Reviews (1): Last reviewed commit: "Completely fill the finalized block node..." | Re-trigger Greptile

y: d.y,
_color: nodeColor(d.data, data),
_ratio: weightRatio(d.data, data.validator_count)
_stroke: nodeStroke(d.data, data),
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.

P1 _stroke is computed but never applied

_stroke is stored on each flat node but the render code references d._color for both the initial enter and the transition update of .node-outer. The nodeStroke function (and this property) are therefore dead code — the darker outline never actually appears.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/static/fork_choice.html
Line: 322

Comment:
**`_stroke` is computed but never applied**

`_stroke` is stored on each flat node but the render code references `d._color` for both the initial enter and the transition update of `.node-outer`. The `nodeStroke` function (and this property) are therefore dead code — the darker outline never actually appears.

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

Comment thread crates/net/rpc/static/fork_choice.html Outdated
Comment on lines +350 to +356
const weightLine = isFinalized
? `<span class="tt-label">status:</span> finalized`
: `<span class="tt-label">weight:</span> ${d.weight}`;
return `<span class="tt-label">root:</span> ${truncateRoot(d.root)}<br>` +
`<span class="tt-label">slot:</span> ${d.slot}<br>` +
`<span class="tt-label">proposer:</span> ${d.proposer_index}<br>` +
`<span class="tt-label">weight:</span> ${d.weight}${total != null ? `/${total} (${pct}%)` : ''}`;
weightLine;
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-finalized tooltip silently drops the weight percentage

The old tooltip rendered weight: X/total (pct%) for non-finalized blocks; the new version shows only weight: X. The total and percentage were useful for understanding a node's relative support — this is a quiet regression for all non-finalized nodes in the tree.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/static/fork_choice.html
Line: 350-356

Comment:
**Non-finalized tooltip silently drops the weight percentage**

The old tooltip rendered `weight: X/total (pct%)` for non-finalized blocks; the new version shows only `weight: X`. The `total` and percentage were useful for understanding a node's relative support — this is a quiet regression for all non-finalized nodes in the tree.

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

@pablodeymo
Copy link
Copy Markdown
Collaborator

Thanks for digging into this — the original observation was correct, and the visual half of the bug is still real. A couple of things have shifted since this was opened, though, so I wanted to flag them before you spend more time on it.

#341 has already addressed the tooltip side

After this PR was opened, #341 (fix(forkchoice-viz): expose all role overlaps per block) landed on main and rewrote tooltipHtml. On current main:

  • nodeRoles() classifies a block by all overlapping roles (finalized / justified / safeTarget / head).
  • The tooltip shows a status: line listing every applicable role.
  • The weight line is skipped for purely-finalized blocks, so weight: 0 no longer appears next to status: finalized.

That means the misleading-tooltip part of #333 is already fixed on main. The tooltip rewrite in this PR would actually regress it: isFinalized = d.slot <= data.finalized.slot collapses to a single status, so a finalized+justified block (or finalized+head) would lose the secondary role label that #341 added.

The disc-fill bug is still real on main

weightRatio() still returns 0 for finalized blocks, so _ratio: 0 makes the inner mask cover the entire disc and you only see the green outline. That's the part of #333 that's still worth fixing, and your PR's nodeRatio is exactly the right idea — it just needs to live on top of #341's role-aware structure.

Suggested reshape

If you rebase against main and reduce the diff to one branch in the flatNodes mapping, the whole fix is a few lines:

const flatNodes = allDescendants.map(d => {
  const roles = nodeRoles(d.data, data);
  const isFinalized = roles.includes("finalized");
  return {
    ...d.data,
    x: d.x,
    y: d.y,
    _color: roles.length > 0 ? COLORS[roles[0]] : COLORS.default,
    _ratio: isFinalized ? 1 : weightRatio(d.data, data.validator_count),
    _haloColors: roles.slice(1).map(r => COLORS[r])
  };
});

That preserves #341's overlapping-roles support and avoids the merge conflict entirely.

Things I'd suggest dropping

  • nodeStroke() (darker stroke on every node). This is unrelated to Fork choice viz: finalized node shows 0 weight #333 and changes the visual treatment of all nodes, not just finalized ones. Worth a separate PR if you want to propose it on its own — happy to discuss the aesthetic separately.
  • The weightRationodeRatio rename and nodeColor extraction. Once the change is collapsed to the _ratio branch above, neither is needed.
  • The hoveredRoot move. No longer required if tooltipHtml is left alone.

Stale review notes

A couple of Greptile findings are no longer applicable, just so they don't muddy the discussion:

  • "_stroke is computed but never applied" — stale. Your ea2b5ac already wired _stroke into both .attr("stroke", ...) calls.
  • "Tooltip silently drops /total (pct%)" — stale. The suffix is preserved in your latest commit.

Minor (only if you keep nodeRatio)

if (!validatorCount || validatorCount === 0) return 0; — the second branch is unreachable since !0 === true. If the function survives the reshape, the guard collapses to if (!validatorCount) return 0;.

Let me know if you'd like to take another pass, or if you'd prefer I take it from here on top of your branch.

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.

Fork choice viz: finalized node shows 0 weight

2 participants