Files
observer/asset/systemd/blackbeard-api.service
rob thijssen 110fbc3631 feat: blackbeard.observer — live Quantus mining leaderboard
Cargo workspace plus a Vite frontend, following ~/git/architecture/generic.md.

Every block header carries its author's wormhole reward preimage in a `pow_`
PreRuntime digest, so authorship for the whole network is derivable from headers
alone — no indexer, no registration, no way for a miner to be left out. That
decoding, the hashrate maths and the telemetry name attribution live in
blackbeard-core with no I/O at all, so the parts that are easy to get subtly
wrong are exercised by unit tests rather than only against a live chain.

The browser holds one WebSocket: snapshot on subscribe, deltas thereafter. The
head stream is itself a push (chain_subscribeNewHeads), so a block reaches the
page the moment the node imports it. Messages are serialised once per broadcast,
and leaderboards are recomputed only for windows a socket is actually watching.
No RxJS — useSyncExternalStore is React's own contract for this.

Verified against the live Planck testnet: 12/12 headers decoded, telemetry names
attributed (quanpool-planck, baba-gorchitsa, …), warm start restoring 84 blocks
and 5 held names across a restart.

Three findings worth recording, all in CLAUDE.md:

- substrate-telemetry sends its JSON in *binary* frames. A text-only client
  connects, subscribes, reports healthy and receives nothing at all — and a
  Python probe hides it, because json.loads accepts bytes.
- Difficulty is a little-endian U512; decoding it big-endian gives a number
  wrong by ~10^150 that still renders fine.
- Planck's real block interval is ~13-15s against a 6s target with enormous
  variance, so a measured interval needs 20 tip samples before it is publishable.

Deploy assets, the Gitea Actions workflow and script/infra-setup.sh are included;
port 25864 is registered in architecture/port-allocations.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi
2026-09-04 12:33:54 +03:00

61 lines
2.0 KiB
Desktop File

# The observer daemon: watches the configured Quantus chains, keeps the
# standings, and serves REST + WebSocket to the site's nginx.
#
# Runs beside quantus-node on the same host and reads its loopback JSON-RPC, so
# it needs no chain data of its own, no credentials for the node, and no route
# to anything but Postgres and the telemetry feed.
[Unit]
Description=blackbeard.observer API (Quantus mining leaderboard)
Documentation=https://git.lair.cafe/Quantus-Network/blackbeard.observer
After=network-online.target
Wants=network-online.target
# Not Requires=: the node restarting must not take the site down. The daemon
# reconnects on its own and keeps serving what it already knows.
After=quantus-node.service
[Service]
# The binary sends sd_notify(READY=1) after it binds, so systemctl restart
# returns when the port is actually accepting rather than when the process
# started.
Type=notify
User=blackbeard
Group=blackbeard
ExecStart=/usr/local/bin/blackbeard-api --config /etc/blackbeard/config.toml
# SIGTERM drains in-flight requests and exits 0. Long-lived WebSockets are what
# this bound is for: without it, a deploy would wait for the last browser tab to
# close.
KillSignal=SIGTERM
TimeoutStopSec=20s
Restart=always
RestartSec=5s
# Hardening per architecture/generic.md §8. Nothing here is relaxed: the daemon
# reads two sockets and one config file and writes nothing to disk.
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
SystemCallArchitectures=native
RestrictNamespaces=true
ProtectClock=true
ProtectHostname=true
ProtectProc=invisible
# ProtectSystem=strict makes everything read-only; the daemon keeps no state on
# disk, so it needs no ReadWritePaths at all. If one is ever added here, ask
# first what state left the database.
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=multi-user.target