blockyard 0.0.1 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +679 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +172 -4
- package/SECURITY.md +38 -0
- package/bin/blockyard.js +40 -0
- package/config/pool-map.json +2620 -0
- package/docs/API.md +1575 -0
- package/docs/ARCHITECTURE.md +1307 -0
- package/docs/AUTO-UPDATE.md +269 -0
- package/docs/CONFIGURATION.md +840 -0
- package/docs/DEFECTS.md +813 -0
- package/docs/EFFECTS-AGENTS.md +448 -0
- package/docs/GETTING-STARTED.md +202 -0
- package/docs/INSTALL.md +490 -0
- package/docs/MEASUREMENTS.md +1254 -0
- package/docs/PRIVATE-LEADERBOARD.md +230 -0
- package/docs/RULES.md +681 -0
- package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
- package/docs/SECURITY-AUDIT.md +258 -0
- package/docs/SECURITY.md +195 -0
- package/docs/STATE-2026-09-09.md +200 -0
- package/docs/TROUBLESHOOTING.md +298 -0
- package/docs/USER-GUIDE.md +1022 -0
- package/package.json +53 -5
- package/public/404.html +9 -0
- package/public/css/app.css +1785 -0
- package/public/index.html +893 -0
- package/public/js/about.js +112 -0
- package/public/js/agents.js +964 -0
- package/public/js/app.js +1312 -0
- package/public/js/arkanoid.js +806 -0
- package/public/js/blockanoid.js +347 -0
- package/public/js/blockout.js +347 -0
- package/public/js/blockpack.js +428 -0
- package/public/js/blockscene3d.js +2678 -0
- package/public/js/breakout.js +224 -0
- package/public/js/charts.js +635 -0
- package/public/js/depthchart.js +311 -0
- package/public/js/details3d.js +2957 -0
- package/public/js/explorer.js +405 -0
- package/public/js/feepalette.js +149 -0
- package/public/js/fmt.js +162 -0
- package/public/js/goggles.js +886 -0
- package/public/js/kiosk.js +41 -0
- package/public/js/login.js +83 -0
- package/public/js/markets.js +357 -0
- package/public/js/mining.js +1138 -0
- package/public/js/panels.js +966 -0
- package/public/js/pricechart.js +188 -0
- package/public/js/settings.js +1014 -0
- package/public/js/tetris.js +226 -0
- package/public/js/tetrust.js +356 -0
- package/public/js/tetsound.js +175 -0
- package/public/login.html +33 -0
- package/scripts/blockfile-measure.js +156 -0
- package/scripts/browser-check.mjs +286 -0
- package/scripts/check.js +173 -0
- package/scripts/decode-check.js +81 -0
- package/scripts/doc-counts.js +109 -0
- package/scripts/donate-qr.py +20 -0
- package/scripts/fake-node.js +534 -0
- package/scripts/index-bench.js +216 -0
- package/scripts/index-benchmark.js +117 -0
- package/scripts/index-build.js +40 -0
- package/scripts/live-render-check.mjs +89 -0
- package/scripts/manage-users.js +132 -0
- package/scripts/motion-check.mjs +138 -0
- package/scripts/pool-map.js +157 -0
- package/scripts/setup.js +410 -0
- package/scripts/shots.mjs +272 -0
- package/scripts/smoke.sh +327 -0
- package/scripts/ui.js +174 -0
- package/server/auth/sessions.js +221 -0
- package/server/auth/users.js +243 -0
- package/server/chain/blockfile.js +234 -0
- package/server/chain/index/build.js +193 -0
- package/server/chain/index/heights.js +36 -0
- package/server/chain/index/live.js +276 -0
- package/server/chain/index/rows.js +145 -0
- package/server/chain/index/store.js +154 -0
- package/server/chain/index/worker.js +109 -0
- package/server/chain/tx.js +310 -0
- package/server/collect/gbt.js +229 -0
- package/server/collect/logparse.js +765 -0
- package/server/collect/logtail.js +189 -0
- package/server/collect/markets.js +333 -0
- package/server/collect/mining.js +333 -0
- package/server/collect/monitor.js +2516 -0
- package/server/collect/nextblock.js +275 -0
- package/server/collect/sync.js +386 -0
- package/server/config.js +620 -0
- package/server/http/api.js +1275 -0
- package/server/http/explorer.js +418 -0
- package/server/http/server.js +412 -0
- package/server/http/sse.js +176 -0
- package/server/http/static.js +212 -0
- package/server/main.js +628 -0
- package/server/netinfo.js +253 -0
- package/server/rpc/allowlist.js +130 -0
- package/server/rpc/client.js +414 -0
- package/server/store/audit.js +148 -0
- package/server/store/history.js +220 -0
- package/server/store/ledger.js +290 -0
- package/server/store/ring.js +173 -0
- package/server/util/fmt.js +29 -0
- package/systemd/blockyard.service +100 -0
package/docs/SECURITY.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Security and privacy
|
|
2
|
+
|
|
3
|
+
BlockYard watches a Bitcoin node and shows what it sees to the people you let in. This
|
|
4
|
+
page describes the access model, what protects it, and exactly what leaves your machine.
|
|
5
|
+
To report a vulnerability, see [SECURITY.md](../SECURITY.md) at the repository root.
|
|
6
|
+
|
|
7
|
+
- [Threat model in one paragraph](#threat-model-in-one-paragraph)
|
|
8
|
+
- [Access: open by default, accounts on request](#access-open-by-default-accounts-on-request)
|
|
9
|
+
- [Accounts, sessions and passwords](#accounts-sessions-and-passwords)
|
|
10
|
+
- [Talking to the node](#talking-to-the-node)
|
|
11
|
+
- [Node writes](#node-writes)
|
|
12
|
+
- [Network exposure](#network-exposure)
|
|
13
|
+
- [Transport security](#transport-security)
|
|
14
|
+
- [Browser security](#browser-security)
|
|
15
|
+
- [Outbound connections](#outbound-connections)
|
|
16
|
+
- [What is stored, and where](#what-is-stored-and-where)
|
|
17
|
+
- [Hardening checklist](#hardening-checklist)
|
|
18
|
+
|
|
19
|
+
## Threat model in one paragraph
|
|
20
|
+
|
|
21
|
+
The monitor is a **reader**. It holds your node's RPC credentials, so the main risks are:
|
|
22
|
+
someone using the monitor to make the node do something (prevented: writes are off, the RPC
|
|
23
|
+
console is read-only behind a default-deny allowlist), someone reading your node's state who
|
|
24
|
+
should not (controlled by where it listens, a CIDR gate, and optional accounts), and the
|
|
25
|
+
monitor leaking information about you to third parties (limited to the on-demand market data
|
|
26
|
+
connections listed below, which you can turn off). The monitor does not hold keys and has no
|
|
27
|
+
wallet access.
|
|
28
|
+
|
|
29
|
+
## Access: open by default, accounts on request
|
|
30
|
+
|
|
31
|
+
With the default `auth.enabled: false`, anyone who can reach the port reads the monitor
|
|
32
|
+
with the fixed role `viewer`:
|
|
33
|
+
|
|
34
|
+
| open to anyone who can reach the port | still closed |
|
|
35
|
+
|---|---|
|
|
36
|
+
| every chart, the sync view, the event stream | user administration |
|
|
37
|
+
| peer, mempool, block and transaction detail, the explorer | the audit trail |
|
|
38
|
+
| the read-only RPC console (behind the allowlist) | password changes and sessions |
|
|
39
|
+
| the live Server-Sent Events stream | **every node write**, even if actions are enabled |
|
|
40
|
+
|
|
41
|
+
The `viewer` ceiling cannot be raised by configuration or by any credential while accounts
|
|
42
|
+
are off. The start-up log states which addresses are readable and how to close them, so
|
|
43
|
+
"anyone on the LAN can read your node" is never a surprise.
|
|
44
|
+
|
|
45
|
+
Set `BLOCKYARD_AUTH=1` (or `"auth": { "enabled": true }`) to require sign-in. Roles:
|
|
46
|
+
|
|
47
|
+
| role | may |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `viewer` | read everything above |
|
|
50
|
+
| `operator` | also run node actions that have been explicitly enabled |
|
|
51
|
+
| `admin` | also manage users and read the audit trail |
|
|
52
|
+
|
|
53
|
+
The last enabled admin cannot be demoted, disabled or deleted.
|
|
54
|
+
|
|
55
|
+
## Accounts, sessions and passwords
|
|
56
|
+
|
|
57
|
+
- **Passwords** are hashed with scrypt (N=16384, r=8, p=1 by default), with a per-user salt;
|
|
58
|
+
a 12-character minimum and common breach-corpus shapes are refused. Passwords are never
|
|
59
|
+
logged, never returned by any API, and stored only as hashes. Each user's scrypt
|
|
60
|
+
parameters are stored with the hash; raising them in the configuration upgrades an
|
|
61
|
+
account on its next successful sign-in.
|
|
62
|
+
- **Sessions** use 32-byte random tokens, stored hashed, in an `HttpOnly`, `SameSite=Strict`
|
|
63
|
+
cookie (`Secure` over HTTPS). A session ends 8 hours after its last request, and 72 hours
|
|
64
|
+
after sign-in whatever happens.
|
|
65
|
+
Roles are re-read on every request, so a demotion takes effect immediately.
|
|
66
|
+
- **CSRF**: every state-changing request must carry an `X-CSRF-Token` header matching the
|
|
67
|
+
session; the cookie alone is never accepted as proof. With accounts off there is no session
|
|
68
|
+
to ride, so the token check has nothing to compare -- and until 2026-09-13 that meant those
|
|
69
|
+
routes had no cross-site protection at all. An audit proved it with a working exploit against
|
|
70
|
+
the node-connection test. Open mode now refuses any state-changing request whose `Origin` is
|
|
71
|
+
not this server, or whose `Sec-Fetch-Site` says cross-site. A client that sends neither header
|
|
72
|
+
(curl, a script) is unaffected: it can already reach the port, and this guards against what a
|
|
73
|
+
*browser* can be made to do on someone's behalf.
|
|
74
|
+
- **Brute force**: sign-in is locked after 8 failures in 5 minutes per username and per
|
|
75
|
+
address, for 10 minutes, with the same error and the same hashing time for unknown users
|
|
76
|
+
and wrong passwords. A separate throttle limits sign-in attempts per address, because each
|
|
77
|
+
attempt costs a deliberately expensive hash.
|
|
78
|
+
- **Rate limits** apply per client address.
|
|
79
|
+
|
|
80
|
+
## Talking to the node
|
|
81
|
+
|
|
82
|
+
- **Read-only allowlist, default deny.** The RPC console and every internal call go through
|
|
83
|
+
an allowlist: read-shaped methods are allowed; wallet, key-material, spending, peer-control,
|
|
84
|
+
chain-mutating and very heavy methods are refused by name — including `getnewaddress` and
|
|
85
|
+
`getrawchangeaddress`, which start with "get" but create keys. Unknown methods are refused.
|
|
86
|
+
- **One request at a time.** The node's RPC server is single-threaded, so the monitor runs a
|
|
87
|
+
single serialized request lane with a minimum spacing, batching, priorities and a stale-drop
|
|
88
|
+
rule. It cannot be used to flood your node. The one exception is the address index build,
|
|
89
|
+
which makes its few cheap calls (block hashes) over a second connection so that the pages are
|
|
90
|
+
not queued behind it — and which pauses itself whenever the first lane sees the node failing
|
|
91
|
+
or answering slowly.
|
|
92
|
+
- **The block files are read directly** (`<datadir>/blocks`), once, to build the address
|
|
93
|
+
index; the node is not asked for them. The index directory is written by the build and by the
|
|
94
|
+
follower, and nothing else in the node's data directory is ever written.
|
|
95
|
+
- **Credentials** are read from the cookie file on demand (it changes on every node restart)
|
|
96
|
+
or from the configured user and password. They are never sent to the browser.
|
|
97
|
+
|
|
98
|
+
## Node writes
|
|
99
|
+
|
|
100
|
+
Node actions (for example `savemempool`) are **off**. Enabling one requires all of:
|
|
101
|
+
|
|
102
|
+
1. `BLOCKYARD_ENABLE_ACTIONS=1`,
|
|
103
|
+
2. the action named in `BLOCKYARD_ACTIONS` (a comma-separated list),
|
|
104
|
+
3. accounts on and a role of at least `operator` — or the deliberate
|
|
105
|
+
`BLOCKYARD_ALLOW_WRITES_WITHOUT_AUTH=1`, which exists so that the combination can only ever
|
|
106
|
+
be chosen on purpose,
|
|
107
|
+
4. a typed confirmation that matches the action's name, per call.
|
|
108
|
+
|
|
109
|
+
Every call and every refusal is appended to the audit trail.
|
|
110
|
+
|
|
111
|
+
## Network exposure
|
|
112
|
+
|
|
113
|
+
- **Bind** only the addresses you mean to serve (`server.host`, a string or a list). See
|
|
114
|
+
[INSTALL.md](INSTALL.md#7-decide-who-can-reach-it) for the options and their consequences.
|
|
115
|
+
- A bind chooses a destination address, not an incoming interface; use a firewall if "LAN
|
|
116
|
+
only" must hold against containers or tunnels on the same machine.
|
|
117
|
+
- **CIDR gate**: `server.allowCidrs` refuses clients outside the listed IPv4/IPv6 networks.
|
|
118
|
+
Membership is computed on the address bytes, and a malformed entry stops the start-up
|
|
119
|
+
rather than silently admitting or refusing everyone.
|
|
120
|
+
- `/api/health` needs no sign-in, so an uptime probe works. It reveals the node inventory,
|
|
121
|
+
version, tip and health to anyone who can reach the port.
|
|
122
|
+
- Behind a reverse proxy, set `server.trustProxy` so rate limits and the CIDR gate see the
|
|
123
|
+
real client address — and only then.
|
|
124
|
+
|
|
125
|
+
## Transport security
|
|
126
|
+
|
|
127
|
+
- HTTPS is built in: name a certificate and key and **every** listener serves HTTPS. A
|
|
128
|
+
half-configured pair or an expired certificate stops the start-up; a certificate close to
|
|
129
|
+
expiry starts with a warning, and the log prints its fingerprint.
|
|
130
|
+
- Over HTTPS the session cookie is `Secure` and `Strict-Transport-Security` is sent with a
|
|
131
|
+
two-day lifetime, without `includeSubDomains` or `preload` — a LAN address can be reissued,
|
|
132
|
+
and HSTS cannot be withdrawn once a browser has it.
|
|
133
|
+
- Plain HTTP is acceptable for a trusted LAN or behind a tunnel; the start-up log says when
|
|
134
|
+
it is serving plain HTTP.
|
|
135
|
+
|
|
136
|
+
## Browser security
|
|
137
|
+
|
|
138
|
+
- **Content Security Policy**: `default-src 'self'`, `frame-ancestors 'none'`, scripts only
|
|
139
|
+
from the monitor itself plus a per-response nonce, and **no inline styles at all**
|
|
140
|
+
(`style-src 'self'` with no attribute exception). Every data-driven style is applied through
|
|
141
|
+
the CSSOM instead of `style="…"` attributes.
|
|
142
|
+
- Also sent: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`,
|
|
143
|
+
`Referrer-Policy: no-referrer`.
|
|
144
|
+
- The browser loads nothing from third parties: no CDN, no web fonts, no analytics. Market
|
|
145
|
+
data reaches the browser only through the monitor's own API.
|
|
146
|
+
|
|
147
|
+
## Outbound connections
|
|
148
|
+
|
|
149
|
+
Everything in the monitor talks only to your node, **except**:
|
|
150
|
+
|
|
151
|
+
| when | to | what is sent |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| while someone has the **Markets**, **Kiosk** or **Overview** tab open — Overview's price line is **on by default**, so the landing page reaches out unless you switch it off — and for 10 minutes after the last request | `api.exchange.coinbase.com`, `api.kraken.com`, `www.bitstamp.net`, `api-pub.bitfinex.com`, `www.okx.com` (HTTPS) | public ticker, hourly candle and order-book requests with a `User-Agent` naming the software — nothing about your node |
|
|
154
|
+
| when someone opens an **explorer** page and no fresh market price is at hand | two of the exchanges above, at most once a minute | a public ticker request |
|
|
155
|
+
|
|
156
|
+
Nothing is fetched when nobody is looking. Your machine's public address is visible to those
|
|
157
|
+
exchanges when a request is made, as with any web request. To make **no** outbound connections
|
|
158
|
+
at all, set `BLOCKYARD_MARKETS=0` (or `"markets": { "enabled": false }`): the Markets and Kiosk
|
|
159
|
+
tabs then say that market data is off, and the explorer shows no dollar figures.
|
|
160
|
+
|
|
161
|
+
The monitor sends no telemetry, checks for no updates, and phones home to no one.
|
|
162
|
+
|
|
163
|
+
## What is stored, and where
|
|
164
|
+
|
|
165
|
+
Everything the monitor writes lives in its data directory (`./data` by default,
|
|
166
|
+
`BLOCKYARD_DATA` to move it):
|
|
167
|
+
|
|
168
|
+
| file | contents | sensitivity |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `users.json` | account names, roles, scrypt hashes and salts | **secret** (mode 0600) |
|
|
171
|
+
| `sessions.json` | hashed session tokens | **secret** |
|
|
172
|
+
| `audit.jsonl` (+ rotations) | who called what and when; rotated by size (8 MiB, 5 kept) | private |
|
|
173
|
+
| history snapshots | chart time series for the retention window (72 h by default) | private |
|
|
174
|
+
| `pool-aliases.json`, `pool-map.json` | optional mining-pool labels: `pool-aliases.json` is human-edited; `data/pool-map.json` is what `node scripts/pool-map.js` fetches, and it overrides the curated `config/pool-map.json` that ships with the code (mempool.space/mining-pools, MIT, 151 pools) | not secret |
|
|
175
|
+
| the address index (`addressIndex`, `data/index` by default) | the explorer's address index: ~124 GB of sorted rows built from the node's block files, plus the follower's `live.log` and `layers/` | public chain data, not secret |
|
|
176
|
+
|
|
177
|
+
`config/local.json` may hold an RPC password; keep it readable only by the service account.
|
|
178
|
+
Both it and `data/` are git-ignored. Display settings (the gear) are stored in
|
|
179
|
+
`config/blockyard.json`, so every browser sees the same board; they change how things are drawn,
|
|
180
|
+
never what is measured.
|
|
181
|
+
|
|
182
|
+
Logs go to standard output (the systemd journal). They record requests, node state changes
|
|
183
|
+
and errors; they never contain passwords, session tokens or RPC credentials.
|
|
184
|
+
|
|
185
|
+
## Hardening checklist
|
|
186
|
+
|
|
187
|
+
- Run as a dedicated, unprivileged account that can read only the node's cookie and its
|
|
188
|
+
`blocks/` directory (and log), and write only its own `data/` and the index directory.
|
|
189
|
+
- Bind the narrowest set of addresses that serves your users; add a firewall rule if needed.
|
|
190
|
+
- Turn accounts on if anyone who can reach the port should not see your node.
|
|
191
|
+
- Use HTTPS, a reverse proxy, or an SSH tunnel on untrusted networks.
|
|
192
|
+
- Leave node actions off unless you have a specific need, and then enable only that action.
|
|
193
|
+
- Set `BLOCKYARD_MARKETS=0` on machines that must not make outbound connections.
|
|
194
|
+
- Keep `config/local.json` and `data/` readable only by the service account.
|
|
195
|
+
- Keep Node.js current within the 22.x line or later.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# State of the project — 2026-09-09 (handoff for an agent with a real browser)
|
|
2
|
+
|
|
3
|
+
> **Superseded 2026-09-14, names only.** This is a dated record and its measurements stand as
|
|
4
|
+
> written. The names in it are the ones in use that day and are no longer current: the project
|
|
5
|
+
> is **BlockYard** (`/storage/blockyard`, systemd `blockyard.service`, env vars `BLOCKYARD_*`,
|
|
6
|
+
> default web port 21000), and the node it watches is Bitcoin Core. `bmc-port-guard.sh` and
|
|
7
|
+
> `/etc/ssl/bmc-local/ca.crt` are real artefacts on the operator's machine and keep their names.
|
|
8
|
+
|
|
9
|
+
Written by the session that built the mining/visualisation layer. Everything below was
|
|
10
|
+
run on this box; the things I could **not** do are listed explicitly at the end. Read
|
|
11
|
+
`AGENTS.md` first for the rules (zero dependencies, RPC-only sources, never fabricate a
|
|
12
|
+
figure, a chart never erases itself).
|
|
13
|
+
|
|
14
|
+
## What is running
|
|
15
|
+
|
|
16
|
+
| Thing | Where | Note |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| bmcmonitor | `https://<LAN address>:8088` | systemd `bmcmonitor.service`, TLS via a **local CA** (`/etc/ssl/bmc-local/ca.crt`), binds the **LAN address only** — loopback, docker bridges and the tailnet address all refuse, by design |
|
|
19
|
+
| Node watched | `bmc-main`, RPC `127.0.0.1:8331` | the node's own esplora REST is on `127.0.0.1:3005` |
|
|
20
|
+
| Log tailing | **OFF** (default since today) | the UI reads RPC only. `BMC_MON_LOG_SOURCE=1` re-enables it for parser work; do not add panels that need it |
|
|
21
|
+
| Block-chain node under dev | `/storage/bitcoinmachinecode` | a *different* session works there; do not restart or reindex it casually |
|
|
22
|
+
|
|
23
|
+
Import the CA or the browser will refuse the certificate. After any deploy, hard-reload:
|
|
24
|
+
asset URLs are stamped `?v=<build>` and the stamp changes when files change, so a tab
|
|
25
|
+
holding an old tab keeps running old code — that has already produced one false report
|
|
26
|
+
today ("still renders nothing" while the fix was on disk and undeployed).
|
|
27
|
+
|
|
28
|
+
## Verify, don't trust this document
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd /storage/bmcmonitor
|
|
32
|
+
npm test # unit + render + contract tests
|
|
33
|
+
bash scripts/smoke.sh # 109 checks against a real server
|
|
34
|
+
npm run counts # do the documented counts match the suite?
|
|
35
|
+
LAN=$(python3 -c "import json;print(json.load(open('config/local.json'))['server']['hosts'][0])")
|
|
36
|
+
BMC_MON_BASE=https://$LAN:8088 BMC_MON_CA=/etc/ssl/bmc-local/ca.crt npm run render:live
|
|
37
|
+
sudo systemctl restart bmcmonitor # deploy after editing public/** or server/**
|
|
38
|
+
sudo journalctl -u bmcmonitor -f # server-side view; the UI records to it
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`render:live` renders **every page** under the DOM stub against the running monitor and
|
|
42
|
+
fails if an asserted card is empty. Today it caught, in order: an uncaught `ReferenceError`
|
|
43
|
+
that killed the entire Overview (two cards "rendered nothing" because one line above them
|
|
44
|
+
threw), a `fmt is not defined` crash on the Node page, and a card that no test had ever
|
|
45
|
+
reached.
|
|
46
|
+
|
|
47
|
+
## What was built today (mining + visualisation)
|
|
48
|
+
|
|
49
|
+
- **Mining page** (`#mining`): block flow (construction → tip → history, the tip explicitly
|
|
50
|
+
labelled `current tip`, colour = age verdict), ghost cards for the next blocks marked
|
|
51
|
+
`not yet assembled`/`estimate`, ancestor-package table (CPFP shape from
|
|
52
|
+
`getblocktemplate…depends`, with `depends` being **indices into the array**, not txids),
|
|
53
|
+
and two treemaps.
|
|
54
|
+
- **Treemaps** (`public/js/goggles.js`): one rectangle per transaction, area = vbytes,
|
|
55
|
+
colour = the feerate it pays, richest-first. The canvas is **the block**: a 14%-full block
|
|
56
|
+
looks 14% full and the rest is drawn blank and labelled. Cells are bounded (400) and the
|
|
57
|
+
remainder collapses into ONE aggregate cell so the block is always whole.
|
|
58
|
+
- **Tip freshness** is judged against the chain's own measured block interval, not a fixed
|
|
59
|
+
clock: amber past ~1× the average gap, red past ~1.5×, floors at 4/8 min. Arrival is only
|
|
60
|
+
counted when the page **witnessed** the height change — a first paint must not claim a
|
|
61
|
+
fresh tip (that bug coloured a 14-minute-old block green).
|
|
62
|
+
- **Overview** gained a compact copy of the block map (`ovGnTreemap`, `w4`, `.treemap.sm`).
|
|
63
|
+
|
|
64
|
+
## Bugs I fixed, so you don't re-diagnose them
|
|
65
|
+
|
|
66
|
+
- `rows` TDZ in `renderOverview` — two `rows` in one function; killed the whole Overview
|
|
67
|
+
render (the Fees card and the "mempool vs blocks" card were collateral).
|
|
68
|
+
- `fmt is not defined` in `panels.js:drawSelf` — the Node page crashed.
|
|
69
|
+
- Treemap `drawFrame` referenced `label` without destructuring → **every partly-filled
|
|
70
|
+
block threw at paint time and the canvas stayed blank**.
|
|
71
|
+
- The treemap tween re-requested animation frames whenever the clock failed to advance
|
|
72
|
+
(hidden/throttled tab) → unbounded loop. Now a frame budget + stalled-clock check, and
|
|
73
|
+
the settled layout always draws.
|
|
74
|
+
- **CSP**: the policy is `style-src 'self'` with no `style-src-attr` allowance (correct,
|
|
75
|
+
tightened earlier today). My new markup emitted `style="..."` and the browser blocked
|
|
76
|
+
**several hundred** applications, so the train and maps lost every colour. All of it now
|
|
77
|
+
travels as `data-pool` / `data-rate` / `data-w` / `data-delay` / `data-rail` and lands on
|
|
78
|
+
`el.style` through the CSSOM (`applyMiningStyles` in `mining.js`, `applyDataSizes` in
|
|
79
|
+
`app.js`). The palette index comes from our own array — a pool name from the node cannot
|
|
80
|
+
become CSS. `test/csp.test.js` now fails if any JS-built markup carries a style attribute.
|
|
81
|
+
- **The Fees card** was found with `getElementById('ovFees').closest('.card')`, and the DOM
|
|
82
|
+
stub answers `closest()` with `null` — so **no test ever exercised that card**. It is now
|
|
83
|
+
`#ovFeesCard`, addressed by id, and asserted in `test/web-contract.test.js`.
|
|
84
|
+
|
|
85
|
+
## Known, open, or unverified — this is what a browser is for
|
|
86
|
+
|
|
87
|
+
1. **Nothing has been seen rendered.** No real-browser harness exists. Layout, colour
|
|
88
|
+
legibility, overlap, and "does the treemap read as a block" are unverified by me.
|
|
89
|
+
2. **Treemaps at `w4`** (33%) on Mining, `w4` on Overview: is 168px/240px high enough for
|
|
90
|
+
the labels, and is the "block unfilled" band legible at that width?
|
|
91
|
+
3. **Legend/label collisions** inside rectangles (rate + `k` vbytes text) at small cell
|
|
92
|
+
sizes, and the `selection ends` / `everything fits in one block` label near the right
|
|
93
|
+
edge.
|
|
94
|
+
4. **Hover tooltips** (`.goggles-tip`) are positioned `left/bottom` — check they don't
|
|
95
|
+
cover the cursor's cell or overflow the card.
|
|
96
|
+
5. **Motion**: the map tweens only when the Mining page is open and the template refetches
|
|
97
|
+
(on load + every 20 s). Confirm the tween is visible, and that `prefers-reduced-motion`
|
|
98
|
+
snaps without losing information.
|
|
99
|
+
6. **`app.js` boot throws under the DOM stub** (`TypeError` at `boot()`, harness-only). Real
|
|
100
|
+
browsers don't hit it; it means a stub element is missing, and it will keep biting anyone
|
|
101
|
+
extending the harness.
|
|
102
|
+
7. Ghost-card forward capacity numbers are **queue-depth estimates**, not node data.
|
|
103
|
+
8. No TLS certificate automation (self-signed local CA), and `:8999` (mempool-backend API)
|
|
104
|
+
is loopback-only by a netfilter rule — separate from this app.
|
|
105
|
+
|
|
106
|
+
## Guardrails that will bite if you ignore them
|
|
107
|
+
|
|
108
|
+
- **Zero dependencies.** No `npm install`. Node builtins and hand-written canvas only.
|
|
109
|
+
- **No host identity in committed files.** `test/privacy.test.js` derives the username,
|
|
110
|
+
hostname forms and interface addresses at run time and fails on them — it caught a
|
|
111
|
+
hardcoded LAN address in one of my scripts today. Use RFC 5737 documentation ranges in
|
|
112
|
+
docs and tests, and read addresses from config/env at run time.
|
|
113
|
+
- **Test counts are generated** (`npm run counts:fix`). Never type them.
|
|
114
|
+
- **Every claim in a comment must be a measurement** with a number and a date. "Seems" is
|
|
115
|
+
not a justification, and this project has twice been burned by a check that passed for
|
|
116
|
+
the wrong reason (a vacuous assertion, a stub that answered `null`).
|
|
117
|
+
- A chart **never erases itself** to show a status; absence renders as `–` with a reason,
|
|
118
|
+
and stale data renders visibly stale.
|
|
119
|
+
- Absent data is **never** filled with a guess: no per-tx `depends` in
|
|
120
|
+
`getrawmempool` on this node, so cluster analysis is only drawn from
|
|
121
|
+
`getblocktemplate`, and the panel says which of the two it is.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Measured in a real browser (2026-09-10, headless Chromium over CDP)
|
|
126
|
+
|
|
127
|
+
`scripts/browser-check.mjs` drives Chromium over CDP with Node's built-in WebSocket: it
|
|
128
|
+
loads the page, samples **canvas pixels with getImageData**, reads inline vs computed CSSOM
|
|
129
|
+
values, measures element rects, collects console/exceptions, and can pass the screenshot to
|
|
130
|
+
the local vision model. This is the first time any of this has been looked at rather than
|
|
131
|
+
inferred. It found five defects that 349 passing tests did not.
|
|
132
|
+
|
|
133
|
+
### Found and fixed
|
|
134
|
+
1. **Shared animation handle.** `goggles.js` kept one module-level `raf` for both maps, so
|
|
135
|
+
each canvas's render cancelled the *other* one's pending frame — and the first frame was
|
|
136
|
+
drawn only inside that callback. The loser painted **nothing**: `__hasData: true`, 401
|
|
137
|
+
rects laid out, 68% of the canvas covered on paper, **zero painted pixels**. Now the
|
|
138
|
+
handle is per-canvas (`c.__raf`) and the settled layout is painted before any animation.
|
|
139
|
+
2. **Tween grew cells from zero size.** With a snapshot arriving every second, the chart
|
|
140
|
+
spent most of its life mostly-cleared with 1px rectangles. Now position interpolates and
|
|
141
|
+
**size never does**; new cells fade in with alpha. A tween must be the difference between
|
|
142
|
+
two visible states, not the thing that makes pixels exist.
|
|
143
|
+
3. **`style-src` was silently vetoing my own markup** (several hundred blocked
|
|
144
|
+
applications): all `style="..."` removed in favour of `data-pool` / `data-rate` /
|
|
145
|
+
`data-w` / `data-h` / `data-delay` / `data-rail` + CSSOM. Guarded by a test that fails if
|
|
146
|
+
any JS-built markup carries a style attribute.
|
|
147
|
+
4. **`.closest('.card')`** made the Fees card invisible to every test (the DOM stub answers
|
|
148
|
+
`null`), so it could render nothing forever. Now addressed by `#ovFeesCard`.
|
|
149
|
+
5. **`rows` TDZ** in `renderOverview` killed the whole Overview. Also: the earlier "fixed"
|
|
150
|
+
report was false because the fix was never **deployed** — deploy is part of a fix.
|
|
151
|
+
|
|
152
|
+
### Still broken, with the measurements
|
|
153
|
+
- **The Mining page block map still paints nothing** (`gnTreemap`: `rects: 401`,
|
|
154
|
+
`distinctColours: 0`, `paintedPixelsPct: 0`) while the mempool map on the same page paints
|
|
155
|
+
100% with the identical code path and the same rect count. I did not find the cause. Start
|
|
156
|
+
by comparing what differs at `drawFrame` time between the two — `geom.unused` is set for
|
|
157
|
+
the block map and absent for the mempool map, which changes `region`; instrument
|
|
158
|
+
`drawFrame` with a counter and a sample of the first rect's `fillStyle`.
|
|
159
|
+
- **The style pass is only half applying.** In the browser: `.bdot` inline style is
|
|
160
|
+
`background: rgb(95, 176, 201)` with **no `--pool`**, and `.bfill span` has `background`
|
|
161
|
+
but **no width**, while `document.querySelectorAll('[data-w]')` counts 27 of which **2**
|
|
162
|
+
have an inline width (the sync hero's, applied by app.js). So `applyMiningStyles` is
|
|
163
|
+
running an older code path than the one on disk and served (`setProperty` and
|
|
164
|
+
`applySizes` are both present in the served bytes, and `Cache-Control: no-cache`). That
|
|
165
|
+
contradiction is unresolved — check whether the module the browser evaluates is the
|
|
166
|
+
module the server sent (compare `import.meta` resolution / a marker constant), before
|
|
167
|
+
touching the style code again.
|
|
168
|
+
- **The flow row overflows its card** and is cut off: `.flow` scrollWidth 2236 inside
|
|
169
|
+
clientWidth 1369. I added `.flow { width: max-content }` + `.flowwrap { overflow-x: auto }`;
|
|
170
|
+
the browser still measured clipping after the change, so either the CSS did not apply as
|
|
171
|
+
written or the grid parent constrains it. Re-measure `.flowwrap` rather than `.flow`.
|
|
172
|
+
|
|
173
|
+
> **Superseded 2026-09-14:** the first two readings above were the harness measuring the
|
|
174
|
+
> previous build, not the page — `Page.navigate` to a URL differing only in its fragment does not
|
|
175
|
+
> reload the document (`docs/RULES.md` rule 25). Re-run against the current build on 2026-09-10,
|
|
176
|
+
> `gnTreemap` and `gnMempoolTreemap` both painted 100% of their pixels.
|
|
177
|
+
|
|
178
|
+
### Notes on the harness itself
|
|
179
|
+
- Chromium only survives inside one shell invocation, so launch + check must run together:
|
|
180
|
+
`rm -rf /tmp/cp; (setsid /usr/bin/chromium-browser --headless=new --no-sandbox
|
|
181
|
+
--user-data-dir=/tmp/cp --remote-debugging-port=9333 --ignore-certificate-errors
|
|
182
|
+
about:blank &) ; until curl -s 127.0.0.1:9333/json/version >/dev/null; do sleep 2; done;
|
|
183
|
+
BMC_MON_BASE=https://<lan>:8088 node scripts/browser-check.mjs mining`
|
|
184
|
+
- Quote `--remote-allow-origins=*` or the shell globs it into filenames.
|
|
185
|
+
- **`Page.navigate` to a URL differing only in `#fragment` does not reload the document.**
|
|
186
|
+
Reusing a warm browser therefore re-measures whatever was loaded before the last deploy:
|
|
187
|
+
`public/**` changes, the page does not, and every reading is of the previous build. The
|
|
188
|
+
harness now appends `?t=<ms>` for this reason. Rule 25: prove the browser is running the
|
|
189
|
+
bytes you served before believing anything it says about them.
|
|
190
|
+
- Do not assert `document.body.firstChild` is an element. Leading whitespace before
|
|
191
|
+
`<header>` is a `#text` node in every ordinary document; a probe that checked this
|
|
192
|
+
"proved" the Overview rendered nothing when it had rendered fine throughout.
|
|
193
|
+
- `Page.loadEventFired` is not reliably seen by the current waiter; the probe's
|
|
194
|
+
**load guard** (abort unless `#nav` and body children exist) is what stops a broken
|
|
195
|
+
invocation being reported as "the page is blank" — that happened once and the vision
|
|
196
|
+
model confidently confirmed a blank white page that was only my own bad URL.
|
|
197
|
+
- Vision: this deployment answers with `content: null` and the text in `reasoning`; reading
|
|
198
|
+
only `content` silently yields an empty review.
|
|
199
|
+
- `mempool-backend.service` has been `failed` since 2026-09-09 16:58 (start-limit hit after
|
|
200
|
+
12 restarts; its own stdout is not in the journal). Unrelated to the above, untouched.
|