Deployment for a Quantus Network Planck node and its external GPU miner.
Two components:
| Component | Role |
|---|---|
quantus-node | consensus, P2P, reward address. Disk + network bound. |
quantus-miner | Poseidon2-over-Goldilocks search. Pure GPU compute. |
Scope: which hosts run what lives in the deploy-node / deploy-miner
matrices in .gitea/workflows/deploy.yaml and nowhere else — that is the
convention (deployment-gitea-actions.md) and it keeps the fleet's shape in one
place instead of scattered through prose. Don't restate host assignments,
hardware inventories, sites or addressing in this file.
One node serves many miners. node/src/miner_server.rs keeps a HashMap of
connected miners and broadcast_jobs the same work to all of them; each miner
picks its own random starting nonce, so there is no coordination or range
allocation. A node per miner would only be needed for a separate reward address.
They are split deliberately. Setting --miner-listen-port on the node
disables its built-in CPU mining, so the node authors nothing unless an
external miner is connected — the split is not optional once that flag is set.
The miner connects out to the node over QUIC/9833 and listens on nothing but a
loopback metrics port.
Conventions: ~/git/architecture — generic.md for the on-host shape,
deployment-gitea-actions.md for the deploy mechanism. Deviations are listed
below.
The proof-of-work is Poseidon2 over the Goldilocks field (p = 2⁶⁴ − 2³² + 1),
width 12, 4 external + 22 internal rounds — an arithmetic hash, not a bit-mixing
one. Cost is dominated by 64-bit modular multiplication, the state lives entirely
in registers, and there is no memory hardness at all. Measured on this fleet:
| Hardware | Hashrate | Power | Efficiency |
|---|---|---|---|
| RTX 5090 | 368 MH/s | 600 W | 0.60 MH/s/W |
| RTX 5090 @ 400 W floor | 281 MH/s | 400 W | 0.70 MH/s/W |
| RTX 4090 | 183 MH/s | 450 W | 0.41 MH/s/W |
| current-gen desktop CPU, 32t | ~4 MH/s | — | — |
| older server CPU, 12t | ~0.5 MH/s | — | — |
Measured on representative hardware, not estimated. A single 5090 at its minimum
power limit out-hashes a couple of dozen server CPUs combined. CPU mining is
therefore disabled everywhere (--cpu-workers 0), including on GPU hosts, where
it would add ~1% hashrate for a couple hundred watts. Older server CPUs are
node-capable and mining-worthless.
GPU/CPU parity is verified upstream (engine-gpu's gpu_cpu_parity example,
25/25 jobs) — the GPU is doing real work, not advancing a lying counter.
Registered in ~/git/architecture/port-allocations.md §5.
| Port | Proto | Component | Exposure |
|---|---|---|---|
30333 | tcp | node P2P (libp2p) | public — named firewalld service, plus an inbound forward at the site edge |
9833 | udp | node ← miner control channel | mesh, firewalld rich rule scoped to the miner host only |
9615 | tcp | node Prometheus | loopback |
9900 | tcp | miner Prometheus | loopback |
Deviation from port-allocations.md §3: these are upstream protocol
defaults, not numbers derived from the service name. 30333 is the chain's
well-known P2P port and changing it buys nothing; 9833 is the miner protocol's
default and both ends must agree. They are recorded in the registry, which is
what the doc says actually prevents collisions. Neither lands in the crowded
alt-HTTP cluster.
The miner link gets a rich rule rather than a plain --add-service because the
fleet uses a single default zone (generic.md §9): adding the service outright
would publish 9833/udp on every address the host carries, and upstream is
explicit that this port must never face the internet.
Applied at boot by nvidia-power-limit.service, configured per host in
infra-setup.sh's GPU_POWER_LIMITS as host=watts, and clamped to each
card's own floor and ceiling — a request the hardware refuses is corrected with
a log line rather than silently ignored.
This is not a compromise. Measured on a 4090 mining Poseidon2:
| limit | hashrate | temp | efficiency |
|---|---|---|---|
| 450 W (stock) | 185.5 MH/s | 70 °C | 0.413 MH/s/W |
| 300 W | 183.2 MH/s | 61 °C | 0.611 MH/s/W |
| 250 W | 180.3 MH/s | 58 °C | 0.721 MH/s/W |
44% less power and 12 °C cooler for 2.8% less hashrate. The workload is register-resident integer math and does not need the top of the clock curve, so the last few hundred MHz were very nearly pure heat. It also cuts current through the 12VHPWR connector, the one component on 4090/5090 cards with a documented melting failure mode.
A boot-time unit is required because nvidia-smi -pl survives neither a reboot
nor a driver unload. The unit also enables persistence mode, which stops the
limit being lost when the last GPU client detaches — a silent reversion that
would otherwise show up only as a warm connector. The dashboard's GPU Power
gauge reads draw against the enforced limit, so a reverted cap appears as the
gauge dropping from ~100% to ~60% rather than as nothing at all.
Scraped by the fleet Prometheus and visualised in Grafana (folder Quantus).
Both exporters are unauthenticated, and the miner's binds 0.0.0.0
unconditionally with no loopback option, so what bounds access in each case is a
firewalld rich rule scoped to the scrape host — not the bind address.
| Asset | Deployed to |
|---|---|
asset/prometheus/quantus.yml.tmpl | /etc/prometheus/scrape_configs.d/quantus.yml |
asset/grafana/quantus.json | /etc/grafana/provisioning/dashboards/quantus/ |
Scrape targets are rendered from SCRAPE_NODES / SCRAPE_MINERS in the workflow
env:, which must agree with the deploy matrices. validate metrics asserts
Prometheus holds every quantus-* target and that all are up, so drift fails
the run rather than quietly producing an empty graph.
Two pieces of shared infrastructure needed one-time changes, which is why
they live in infra-setup.sh --metrics-hosts rather than the workflow:
prometheus.yml gained scrape_config_files: so each project can own a drop-in
instead of every project editing one file, and grafana.container gained a
dashboards bind mount — it previously mounted only the datasource. Both are
additive, guarded, and back up what they touch.
What the dashboard cannot tell you: neither binary exports a difficulty metric or an authored-block counter, and the miner exports no solutions-found counter — so "do we win the share of blocks our hashrate predicts?", the only honest measure of orphan and stale-work loss, is not plottable today. The "Mining efficiency" row shows the measurable proxies and says so explicitly. Closing that gap, and the experiments that depend on it, are tracked in issue #1.
The dashboard is provisioned with allowUiUpdates: false: edit the JSON here and
redeploy, so a browser tweak cannot silently diverge from source.
Planck is a testnet. Mainnet is expected 2026-09-09, and this repo exists so that switching is a config change rather than a scramble:
| To switch | Change |
|---|---|
| Chain | CHAIN: in the workflow env: — the unit reads it from config, and the miner credential path <base-path>/chains/<chain>/ follows from the same value |
| Versions | NODE_VERSION / MINER_VERSION pins |
| Reward address | inner_hash per node in the matrix |
Still to decide before launch day, none of which the deploy can settle for you:
inner_hash was derived for testnet.
A dedicated mining wallet is the recommendation either way
(doc/wormhole-rewards.md §6).CHAIN creates a new
chains/<chain>/ directory and leaves the old one on disk; the testnet chain
reached ~1M blocks, so check free space rather than assume.Sync at launch is trivial — a new chain starts at genesis — so the long testnet sync is not a preview of mainnet day.
wss://quantus.internalsubxt — and therefore quantus-cli — refuses plain ws:// to any host but
localhost (InsecureUrl), so without TLS every client needs an ssh tunnel:
ssh -f -N -L 19944:127.0.0.1:9944 <node>
quantus balance --node-url ws://127.0.0.1:19944 --address <addr>
The tunnel-free answer is a vhost on the site's edge proxy, per
architecture/reverse-proxies.md and internal-tls.md:
./script/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub # includes the edge role
It mints an internal-CA cert for quantus.internal, installs
asset/nginx/quantus.internal.conf, arms [email protected] for renewal, and
reloads nginx. Operator-run, never CI-deployed — the runner has no rights to
read certificate keys or reload nginx on a shared edge proxy.
Split-horizon DNS is a separate manual step (the record lives in the site router's Unbound, not here):
opn-cli --config ~/.opn-cli/<site>.yml unbound host create \
--hostname quantus --domain internal --server <edge mesh ip>
Two traps worth knowing:
listen 127.0.0.1:14443 ssl proxy_protocol;, not listen 443. A stream
SNI router owns TCP 443 on the edge proxies and hands non-passthrough names to
the local https tier. Copy the listen line from a currently enabled
vhost — on hanzalova, 11 of 19 enabled vhosts are regular files that have
drifted from their sites-available counterparts since that migration, so the
sites-available copy can be stale and wrong.--rpc-methods safe blocks the
dangerous calls, but anyone reaching the name reads all state and can submit
signed extrinsics. .internal is mesh-only and must never get a public record.Loopback by default, which is the right posture for a validator. Opening it needs three things, and the flags are the part people forget:
--unsafe-rpc-external bind beyond loopback (see below)
--rpc-methods safe NOT auto, NOT unsafe
--rpc-cors all only if browsers will call it
--rpc-rate-limit 300
--rpc-rate-limit-whitelisted-ips 127.0.0.1/32 or the local exporter throttles itself
Set them as a complete string in the node matrix's rpc_expose; empty leaves it
on loopback. The deploy then opens 9944 with a firewalld rich rule scoped to
the node's own /16, derived from its own address at deploy time — so no
subnet literal lives in this repo and the rule follows the host between sites.
--rpc-external does not work on a validator. Substrate refuses it and
exits — whatever --rpc-methods says — demanding --unsafe-rpc-external or
--rpc-methods=unsafe as an explicit acknowledgement. The two binding flags are
identical ("Same as --rpc-external" per --help); only the name differs. So
the correct pairing is the alarming-looking --unsafe-rpc-external with
--rpc-methods safe, which binds externally while still refusing every unsafe
method.
safe is the load-bearing one. It still permits every state read and
author_submitExtrinsic (signed, so no key exposure), while blocking
author_rotateKeys, system_addReservedPeer and the rest of the unsafe set.
--rpc-methods auto happens to downgrade to safe once listening externally,
but relying on that means the safety depends on a flag nobody wrote down. Never
unsafe on a validator.
The rate-limit whitelist matters more than it looks: the arena exporter runs on the node host and makes about four RPC calls every two seconds, which is 120 a minute — enough to throttle itself against a naive limit.
9944 is HTTP and WebSocket on the same port, so ws://<node>:9944 and
http://<node>:9944 both work. It must never be reachable from the WAN.
Opening 30333 on the host is only one of three things a node needs to accept
inbound peers:
opn-cli exposes firewall alias and firewall rule but no NAT
subcommand, so a port forward is a console change.public_addr in the deploy matrix. Behind NAT, libp2p advertises the
address it can see — a mesh address no external peer can dial. A forward gets
packets in but never tells anyone to send them. Prefer a /dns4/ site
indirection name over a literal address so a WAN IP change needs no redeploy.Leave public_addr empty for a deliberately mesh-only node; the unit takes it as
a bare $VAR that systemd word-splits, so empty yields no flag. Validate reports
which of the two states a node is in, because an undialable validator still mines
and still looks healthy — it just takes from the network without adding to it.
| Name | What | Where |
|---|---|---|
RSYNC_SSH_KEY | the shared fleet runner key | Gitea secret — already exists, do not rotate |
inner_hash | 32-byte wormhole preimage, one per node host | workflow deploy-node matrix — public, not a secret |
The inner_hash is not secret. The node embeds it verbatim in the
PreRuntime digest of every block it authors
(client/consensus/qpow/src/lib.rs:726), and the mining-rewards pallet reads
that digest back and derives the payout address on-chain. It is published from
your first mined block onward, so it is configuration, and it lives in the
deploy-node matrix with the other infra truth.
One inner_hash per host — never share one. The value is static (derived per wallet, not per block), so two nodes carrying the same inner_hash are publicly and permanently identifiable as the same operator, which collapses the independence that running nodes at separate sites is supposed to provide. The matrix carries one per entry for that reason.
Publishing it is not a theft risk — spending needs a plonky2 proof of knowledge
of the underlying secret, and recovering that from the inner_hash means
inverting Poseidon2. But it does make mining income permanently attributable:
the privacy in this design is at the exit, not at receipt. The full analysis,
with source citations, is in doc/wormhole-rewards.md.
That on-chain derivation is also why the node wants a preimage rather than an
address: the runtime does the deriving, which is what proves the payout target
is a canonical wormhole address rather than an arbitrary account. Passing an
address would give the runtime nothing to verify. --rewards-inner-hash that
isn't a canonical Poseidon digest is rejected at startup.
Knowing the preimage does not let anyone spend the rewards — spending authority is the wallet key, which never goes near this fleet. Deriving an inner_hash for a wallet you already control is a one-time offline step, done where the seed already lives:
quantus-node key quantus --scheme wormhole --words < mnemonic.txt
The binary refuses --words/--seed as command-line arguments, since argv is
world-readable and lands in shell history and audit logs. Nothing in this repo
needs, wants, or accepts a seed phrase.
The miner's credentials — miner-auth-token and miner-tls-cert-sha256 — are
genuine secrets, and are the only ones here. The node generates them on first
start and regenerates them if its base-path is wiped, so the deploy copies them
from node to miner on every run. That makes the pairing self-healing instead of
a silent auth failure six months later.
One-time, from a workstation with sudo ssh to the targets:
./script/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub
Re-run it whenever the deploy gains a new file to ship — each job preflights the
target's sudoers against the grants in infra-setup.sh and fails up front,
naming the missing paths, rather than dying partway through an rsync.
Then set each node's inner_hash in the deploy-node matrix. Derive one per
host, offline, from a dedicated mining wallet — see
doc/wormhole-rewards.md §6 for why that matters more than being careful.
Then push to main, or run the deploy workflow from the Actions UI. It has
two modes:
miner_hashes_total is advancing.A deploy that changes nothing restarts nothing. Files are compared by
checksum, not rsync's default size+mtime — fetch re-downloads the binaries
every run, so their mtimes are always new and the default heuristic would report
a change on every deploy. Only a genuine content difference in the binary, unit
or config triggers a restart; firewalld definitions are reloaded separately and
never bounce the daemon. A unit that is unchanged but stopped is started.
That matters more than tidiness: bouncing a syncing node costs it every peer and a RocksDB reopen, and this workflow is also the thing you run to check an untouched deployment is still healthy.
systemctl is-active alone is not evidence this stack works: a miner that found
no GPU adapter, and a node that is authoring nothing because no miner is
attached, both look perfectly healthy to systemd. Hence the counter check.
Upgrades are deliberate — bump NODE_VERSION / MINER_VERSION in the workflow.
There is no auto-update on a validator on purpose.
A miner host may already be running helexa neuron against the same GPU
(architecture/generic.md §11, inference.md). They contend directly: the miner
drives the GPU to ~95% utilisation and near its power limit continuously, which
makes inference latency on that host by turns awful and unpredictable.
Nothing here resolves that, and the deploy does not try to arbitrate. Options, in rough order of preference:
--gpu-throttle-ms, or an ExecCondition that
refuses to start while a model is loaded.Check what a candidate miner host is already serving before enabling
quantus-miner.service on it, not after.
runs-on: infra, not fedora-43. gitea-runners.md §4 says a deploy
needs only ssh + rsync and so fits fedora-43. That is about tooling, not
routing — these targets are mesh-only .internal names, and lair/mail's two
working deploys both use infra for that reason. Revisit if the fedora runners
gain mesh reachability.MemoryDenyWriteExecute=false on the node (Substrate JITs its wasm runtime
through wasmtime) and PrivateDevices=false on the miner (needs
/dev/nvidia*). Both are annotated in the unit files; neither is tidy-up-able.Type=simple, not notify — quantus-node does not sd_notify(READY=1).generic.md §1–§2 do not apply.MINING.md is the real doc)Note that Planck is a testnet: "tokens have no monetary value". Mainnet difficulty and reward schedule are unpublished, so no profitability claim here is possible yet. Also worth tracking: this PoW has no memory hardness and a small fixed state, which makes it an unusually clean FPGA/ASIC target — relevant to any decision to buy hardware for it.
27 activities
e178bcb feat: boot-time GPU power limits, clamped to hardware floorsI said "ten commits are outstanding, several touching the node unit or its config". That was wrong — I was quoting a stale count from my own commit log rather than checking origin/main.
Actual state at the time of writing: one unpushed commit, e178bcb (boot-time GPU power limits).
It touches only asset/gpu/*, asset/systemd/nvidia-*, script/infra-setup.sh and readme.md. Nothing under the node's unit, binary or config, and its effects are already applied to the hosts by hand.
So pushing it during the baseline window is safe. The deploy triggers (asset/** matches the paths: filter) but is content-addressed — push() compares by checksum, nothing the node depends on has changed, RESTART stays 0, and systemctl restart quantus-node is not called. Peer occupancy is undisturbed.
The general rule still holds and is worth keeping: a push that changes the node's unit, binary or rendered config restarts it and voids the arm. Check what a pending commit actually touches before assuming either way — the deploy log's changed: lines say exactly what moved.
Earliest the --out-peers change should be applied: 2026-09-02T03:46Z.
GPU power limits were reduced immediately before this, so every earlier hashrate figure in this issue is stale. The baseline must run against the new limits or the expected-share denominator is wrong.
| host | GPU | power limit | hashrate | temp | | --- | --- | --- | --- | --- | | benjy | RTX 4090 | 250 W (was 450) | 174.8 MH/s | 57 °C | | quadbrat | RTX 3060 | 130 W (was 170) | 42.0 MH/s | 76 °C | | beast | 2× RTX 5090 | 400 W (clamped from 300; floor is 400) | not mining | idle |
Ours: 216.8 MH/s. --out-peers and --in-peers at their defaults of 8 and 32.
difficulty 14,875,185,846
network hashrate 1,367.7 MH/s
block interval 10.876 s (target 6 s — the chain is running slow)
observed share 15.79 %
expected share 15.85 % (216.8 / 1367.7)
reward balance 228.105 PLK
peers 25
outbound held 4 of 8
Observed and expected within 0.06pp of each other at window open. That is a coincidence of timing rather than a result — the window is what produces a result.
Note outbound held is 4 of 8, not saturated at this instant, where it was 9 of 8 earlier. Occupancy fluctuates as peers rotate; the case for raising the cap rests on it sitting at the cap over hours, which is what this window will show. If outbound averages well under 8 across 24h, the whole experiment is moot and that is a valid outcome to record.
Do not push. Ten commits are outstanding, several touching the node unit or its config, so a deploy restarts the node and drops every peer — which resets peer occupancy and voids the arm.
If those commits should land sooner, the honest sequence is: push, wait ~30 minutes for peers to re-establish, then reopen the window with a new timestamp. Losing a few hours of baseline is cheaper than comparing across a restart.
Nothing else in flight touches the node: GPU power limits are applied by nvidia-smi without restarting the miner, and the arena exporter reads over RPC.
--out-peers 24 in the node matrix, deploy, wait ~30 min.Sample size caveat stands: ~8,000 network blocks and ~1,250 of ours per arm, so a few-percent effect sits near the noise floor. The decision of whether a 1–2% difference would change anything should be made now, before the numbers exist.
tuesday, september 1, 2026 — 03:46:58 utc298dff9 feat: at-a-glance GPU gauges598aea2 feat: GPU metrics via the node-exporter textfile collectord784db8 feat: peer occupancy panels for the out-peers experimentThe original recommendation — raise --in-peers to 64, leave --out-peers alone — was backwards. Do not run it as written.
I used cumulative connection opens: 63 inbound against 5 outbound, concluded inbound dominated, and inferred --in-peers was the binding cap.
Cumulative opens measure churn, not occupancy. Inbound connections arrive and leave constantly; outbound ones are deliberately maintained and stable. So a high inbound open count says nothing about whether the inbound cap is full.
opened − closed per direction, against the cap. Now that the node is synced and dialable:
direction opened closed currently held cap
in 170 156 14 32
out 36 27 9 8 <-- SATURATED
Also: substrate_sub_libp2p_peerset_num_discovered = 300.
--in-peers would change nothing — inbound is demand-driven, bounded by how many peers choose to dial us, not by our ceiling.Change --out-peers from 8 to 24. Leave --in-peers at its default.
Rationale: with 300 discovered peers and 8 dial slots, more outbound links mean learning about new tips from more independent sources, which is the mechanism that shrinks the stale-tip window. Keep it modest — this chain uses Dilithium for p2p identity, so each peer costs more than on a typical Substrate chain. 8 → 24 is defensible; 8 → 100 is not.
The gain remains bounded: this reduces wasted work, it cannot increase hashrate share. Expect a few percent, and expect it to be hard to distinguish from noise.
Do not change anything yet. Take a clean 24h baseline first — a stable node with no config changes, since every change restarts it and drops every peer. The exporter now measures what is needed:
| what | metric |
| --- | --- |
| observed share | quantus_share_observed |
| expected share | sum(miner_hash_rate) / quantus_network_hashrate |
| stale-tip proxy | rate(substrate_proposer_block_constructed_count[5m]) * 60 |
| peer occupancy | opened − closed per direction, as above |
| network context | quantus_difficulty, quantus_block_interval_seconds |
Then: change --out-peers, hold 24h, compare the same five.
Sample size still governs the answer. At ~10s blocks and ~16% share, 24h is roughly 8600 network blocks and ~1400 of ours — enough that a few-percent effect is near the noise floor. Decide before looking whether a difference that small would change anything you do; if not, the honest outcome is "no measurable effect", and that is a result worth recording rather than a failure.
tuesday, september 1, 2026 — 03:04:28 utc867a71f feat: telemetry census — node population vs distinct authorsd60173f feat: wss://quantus.internal via the site edge proxy71bbc2e fix: --rpc-external is refused on a validatoradd2630 feat: preflight the sudoers before deployingaa506b9 feat: optionally expose the JSON-RPC to the site LANca94e3a feat: track accrued rewards in the arena exporteradc04f5 fix: wait for first scrape before judging target healthc748da0 feat: arena exporter — network hashrate, difficulty, authorship leaderboard938941b feat: second miner on quadbrat, and make the chain configurableQuantus-Network/qsafe.afAn earlier version of exactly this already exists — a Substrate explorer written against the pre-Planck Quantus protocols. Archived, last pushed 2025-10-13, 101 files, Vite + React + TypeScript (which matches generic.md §4 anyway). Live at https://qsafe.af.
Already built, and directly relevant:
src/components/MiningStats.tsx — miner leaderboard with per-miner block counts and percentage sharesrc/components/Nodes.tsx — telemetry websocket client, same subscribe:<genesis> protocol confirmed abovesrc/chains.ts — per-chain config: genesis, RPC endpoints, indexer, treasury, telemetry URLSo the frontend, the telemetry handling and the leaderboard UI do not need inventing. Three things need changing.
schrodinger: {
endpoints: ["wss://quantu.se"],
indexer: "https://quantu.se/graphql",
telemetry: "wss://tc0.res.fm/feed",
}
MiningStats derives miners from balance events (event.balanceEvent?.account?.id) — the reward deposit credits the miner — which needs either an indexer or a lot of browser-side block fetching. The code carries a zero-address guard and a first-valid-miner-per-block dedup pass, which reads like it was fighting event ambiguity.
The PreRuntime digest approach in the issue above is strictly better for this: headers only (no full blocks, no metadata, no event decoding), unambiguous by construction, and it is what the runtime itself does. It also removes the indexer dependency entirely — which matters, because we would otherwise be depending on quantu.se staying up.
Suggested split: compute the leaderboard in an exporter on our own node from headers, serve it as metrics/JSON, and let the SPA read that. Then the arena has no third-party runtime dependency at all.
schrodinger (Quantus Testnet), resonance, heisenberg. Planck needs adding, then mainnet after 2026-09-09:
genesis: 0x4901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72
telemetry: wss://feed-telemetry.quantus.cat/feed (was wss://tc0.res.fm/feed)
endpoints: ???
endpoints is the open question — a browser SPA needs a public wss RPC. Ours is bound to 127.0.0.1:9944 and should stay that way. Either find a public Quantus RPC, or have the exporter serve what the arena needs so the SPA never talks to a node directly. The second is preferable and follows from §1.
Needs unarchiving upstream, or forking to lair/. Given the chain-side changes and the shift away from the indexer, a fork we control is probably the honest option — with a note in the fork pointing back, per generic.md §11 Source hosting.
Unchanged in substance, but cheaper than estimated: the exporter is still the pre-launch work, and the prior art means the presentation layer is mostly a config-and-refit job rather than a build. The leaderboard maths, the telemetry client and the UI already exist and were written by the same person who now wants them back.
monday, august 31, 2026 — 16:51:24 utcbbe29ca feat: mining efficiency panels, and say what they cannot measure2cd4786 fix: validate metrics without shell-escaped python or grafana auth76fd807 fix: gitea_ci needs a login shell2c896ed fix: ship the grafana dashboard provider, and assert it loaded6e93184 feat: scrape node and miner metrics, and a grafana dashboard2855bd9 fix: do not restart when nothing changedd198ae0 fix: resolve the node address for the miner at deploy time36a9868 fix: wait for the node to bind before asserting it has83f3994 feat: advertise the office indirection name for P2P3740bfb feat: advertise a public address, optionally08d2842 fix: node needs AF_NETLINK, and validate needs honest pipes5b6fb43 fix: quote firewalld rich rules for the remote shell4a9b09f feat(deploy): update node service name to baba-gorchitsacdfebed feat(deploy): update inner_hash for bob.hanzalova.internal942bbb2 docs: keep the fleet's shape out of prose57a3705 feat: per-host inner_hashes, and document the wormhole schemee936597 fix: the inner_hash is public config, not a secret