silQ Road

A block explorer for a chain where value is private by default.

One static directory, one WebSocket to a node, no server-side indexer and no third party. Every number on every page is read live from the node the page is configured with, and the only other requests are to the host serving the page, for config.json at startup and for the page's own assets.

explorer.qnero.io testnet, live

The design constraint

An explorer for a private chain is as careful about what it declines to show as about what it shows. A heuristic that looked like it worked would be a privacy regression shipped as a feature. The refusals below are each a decision, and each has a reason.

01 / Pages

Six routes

Each route, and what it shows
Route What it holds
#/ Head and finalized height, rolling block time, difficulty and an estimated hash rate, the RandomX seed height and its next rotation, tree leaves and depth, the settled nullifier count, pool value, the newest block's coinbase, and a live list of recent blocks
#/blocks A paged list, newest first
#/block/<height or hash> Header fields, the coinbase note, every settlement in full, every shield entry, refused calls, and every extrinsic summarised
#/settlement/<extrinsic hash> One settlement: its slots, the anchor window, and what it publishes and does not
#/search?q= A height, a block hash, an extrinsic hash, a nullifier or a commitment
#/reveals What an observer learns per block, and what stays hidden

Routing is in the fragment, so any static host serves it with no rewrite rule and a pasted link survives a refresh.

02 / Refusals

What it will not do

  • It never asks for a Merkle proof. That call names one leaf to whoever runs the node, which is the correlation a wallet's local tree rebuild exists to avoid. An explorer making it for a viewer would hand the node a per-viewer leaf-interest log. Leaves and the root are read as public ranges, and a lint rule fails the build on the spellings of the call it knows. A syntax fence fails open: a template literal or an aliased reference would pass it, so the rule is a reviewer's aid and never a boundary, and the property rests on the reads the code makes.
  • It does not read a settled nullifier as a note that was spent. A leaf slot has two input positions and its two nullifiers mark both consumed. A position holding a real input spends one note; a position holding a dummy input publishes a nullifier over no note; the two are the same uniform hash in the public record. At least one position of a settled slot is real, so a slot spends one note or two, and the home page's figure bounds from above the notes this chain has spent.
  • It renders a slot's two outputs unordered. Which one is the sender's change is hidden because the wallet draws the payment's output slot per spend. Ordering them, or labelling one "to" and one "change", would reintroduce by presentation what the protocol pays to hide.
  • It has no miner table. The author label is H(cvk, parent_hash), the miner's coinbase viewing key hashed with the block's own parent, so it changes every block and grouping by it groups nothing.
  • It does not reprint a refused call's arguments. A transparent transfer the runtime's filter refuses still enters a block and its arguments stay in the body forever. The block page names the call, says why the arguments are public, and leaves them where the chain put them.
  • It does not sort by ciphertext size or by anchor gap. Both are documented open leaks. The size is shown per leaf and a size other than 1792 bytes is marked, because that is worth knowing. Neither is a sortable column.
  • It does not decode a settlement's anchor height. The anchor is a public input inside the proof. The settlement page states the window the chain enforced, which is what the site can establish from chain state alone.
  • No analytics, no fonts, no images, no CDN. The only requests the page makes are to the configured node and to the host serving the page.

Reads whose request carries a viewer's own 32 bytes, the nullifier lookup and the block-hash header check among them, are printed as a warning first and run only when a reader presses the button.

03 / Run it

Build, configure, deploy

Start a local node first. The repository README carries the build preconditions, and the commands are below.

The dev server runs on http://127.0.0.1:5173 and reads public/config.json, which points at ws://127.0.0.1:9944 out of the box. npm run build produces dist/: an index.html, one JS bundle, one stylesheet and config.json. Asset URLs are relative, so the directory works at a domain root and in a subdirectory with no rebuild.

The bundle is about 1.2 MB, 440 kB compressed, and almost all of it is the polkadot API, which carries the SCALE codec and the type registry the event decoding needs.

One runtime JSON

config.json sits beside the built assets and is read at startup, so one build serves a devnet and a testnet. Nothing about a chain is compiled in. The endpoint must be ws:// or wss://, because the live head is a subscription. A page served over HTTPS cannot open a plain WebSocket, so put the node behind the same TLS the site uses.

The defaults: 12 recent blocks in the home list and in the rolling block-time window, a 512-block search window before a walk gives up, and 25 pages of 1000 keys before the nullifier count reports a floor.

Run it
cd explorer
nice -n 19 npm ci
nice -n 19 npm run dev
04 / Seams

Two things a generic Substrate client gets silently wrong

The header. zkTreeRoot sits between extrinsicsRoot and digest, and a generic decoder reads the digest out of the wrong offset. Block hashes are Poseidon2 over a felt encoding.

The body. The ML-DSA-87 signature is a fixed 7219-byte array, and the polkadot API refuses any fixed array above 2048, so the envelope is walked by hand with the signature lengths and the extension list read out of metadata.

Everything else is metadata-driven, and the assumed storage hashers are asserted against metadata at startup, because an absent key and an empty map are indistinguishable and the difference renders as "0 leaves, 0 nullifiers" with no error.

05 / The house rule

The same rule binds this site

No analytics, no fonts, no images, no CDN is the explorer's rule, and this site lives by it. These pages are hand-written HTML and one stylesheet. There is no build step, no framework, no font download, no tracker and no third-party request of any kind. The two scripts are the theme bootstrap and the theme toggle, and every page reads with both switched off.

Read the page source. It is the same discipline the apps are held to, and it is checkable in one view.