Skip to content

A browser sandbox for the block tree layout, with hand-built chains #161

Description

@b10claude

The block tree layout in www/js/blocktree.js is the most intricate part of the frontend and the part with the least test surface: there is none. It is also the part where bugs are only visible as a picture — a block half a slot too close to its sibling, a chain that slides sideways for no reason, a label drawn over another one. Reviewing #159 meant building a throwaway harness to see the change at all, and that harness turned out to be far more useful than the review it was built for. It's worth having as a permanent tool, and worth extending so chains can be built by hand rather than only from canned fixtures.

What the throwaway version did

A static page that draws blocktree.js against handmade data, with no node, no database and no live stratum feed behind it:

  • A fixture module producing a state_data in the exact shape of api/<n>/data.json (header_infos + nodes), plus a state_stratum_jobs map in the shape main.js builds from the SSE feed.
  • A pane page that declares the globals blocktree.js reads out of main.jsMINING_ENABLED, state_data, state_stratum_jobs, STRATUM_JOB_TTL_MS, state_selected_network_id, nodeInfoRow, copyToClipboard, update, run — then loads blocktree.js and calls draw().
  • Two such panes side by side in iframes, one per git revision of blocktree.js, with pan/zoom relayed between them over postMessage so both always frame the same patch of layout.
  • A readout in each pane that re-runs preprocess_data() and reports, in pixels, how far each block from the feed sits from its siblings. That turned "the block looks too close" into "60.0px apart, and a slot is 120px" — and after the fix, 120.0px.

That last part is the bit I'd keep above all others. The layout's invariants are numeric (NODE_SIZE slots, BLOCK_SIZE faces, sibling separation, pinned-vs-unpinned positions), so a page that measures the layout instead of only drawing it is one step away from being an actual regression test.

What to build

A sandbox page served out of www/ — so it comes up at /sandbox.html on any running instance, needs no separate server, and no special browser. Just open it.

The new part is building the tree by hand instead of picking from a list of fixtures:

  • Click a block to attach a child to it. Attaching to anything other than the current tip creates a fork; that is the whole point.
  • Per-block editing: height, miner, version (so the BIP-9 signalling chips from Support showing tags for BIP-9 style version bit signaling (initially for BIP-110) #156 can be exercised), difficulty_int (MIN_DIFFICULTY gets the accent stroke), and the hash — auto-generated, but editable so a specific sort order can be forced.
  • Mark any block as a tip reported by one or more fake nodes, with a status from active / valid-fork / valid-headers / headers-only / invalid, and a count, since the tip status boxes stack by node count.
  • Attach a stratum job to any block: pick a pool name and the block it's mining on. That covers the ordinary being-mined block, the "just found" stand-in (job.height == max_height + 2 against a parent we don't know), and the fork-the-tip case from fix: draw a to-be-mined block that forks the tip as a fork #159, without waiting for the real feed to produce one.
  • Delete blocks, and a "reorg" action that moves the active tip to another branch, so the transition animation can be watched deliberately rather than at 3am.
  • A countdown height, since state_data.countdown changes what isInteresting() keeps.

Everything should round-trip through the URL or an export/import JSON blob, so a scenario can be pasted into an issue or a PR review. Being able to paste in a real data.json from a live instance and then edit it by hand would cover the "reproduce what I just saw in production" case.

Notes for whoever builds it

Things the throwaway version ran into, so they don't have to be rediscovered:

  • blocktree.js is plain script scope with module-level state bound to #drawing-area — two copies cannot coexist in one document. Iframes are what make a side-by-side comparison possible, and they're cheap. If a before/after mode is kept, the "before" copy has to be a checked-out copy of the old file; a small script that extracts one from a given revision is enough.
  • The root header's prev_id must be the MAX_USIZE sentinel (18446744073709551615), which is what d3.stratify() keys the root off.
  • stripUninteresting(treeData, 4) collapses linear runs longer than four, so a hand-built chain needs a real length before it looks like the production view, and a block's status (via a node tip) is one of the things that makes it survive the collapse.
  • Blocks sort by (height, hash), so which side of a fork a branch lands on is decided by the hash — hence wanting to edit it.
  • The feed's prev_hash arrives in header (little-endian) word order and main.js reverses the words before it ever reaches state_stratum_jobs; the sandbox should store the display-order hash directly and skip that conversion.
  • from_stratum_feed() keys off the synthetic statuses in MINING_TAGS (mining, just-found), and those blocks are given synthetic mining-/found- hash prefixes deliberately so they can't collide with a real header once a node reports it.
  • The orientation is chosen from the window aspect ratio at load; in a narrow iframe that silently picks bottom-to-top. Both orientations are worth a control, since they have separate label and link geometry and bugs show up in only one of them often enough.

Not needed: any headless browser, screenshot tooling, or a driver. This is a thing to open and look at. If it later grows assertions, the measurement readout is the seam to build them on — and at that point a headless run becomes a genuine test rather than a way of taking pictures.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions