Adds a Mining efficiency row: mining rounds started per minute, hashes per mining round, and active jobs over time. These are proxies, not the answer. Neither binary exports what the question actually needs -- quantus-node has no difficulty metric and no authored-block counter (substrate_proposer_block_constructed counts proposals constructed, not rounds won), and quantus-miner has no solutions-found counter. So authored-blocks-vs-expected-share cannot be plotted, and a panel implying otherwise would be worse than none. A text panel on the row states that plainly rather than leaving someone to infer a conclusion the data does not support. lair/quantus#1 tracks closing the gap -- a chain-query exporter counting blocks credited to the reward address is the cheapest route -- and the experiments that depend on it.
13 KiB
quantus
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.
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.
Why GPU, and why only these hosts
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.
Ports
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.
Monitoring
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.
Being dialable
Opening 30333 on the host is only one of three things a node needs to accept
inbound peers:
- firewalld on the host — the deploy does this.
- An inbound TCP forward at the site edge to the node host. Not managed
here;
opn-cliexposesfirewall aliasandfirewall rulebut no NAT subcommand, so a port forward is a console change. public_addrin 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.
Secrets, and one thing that only looks like one
| 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.
Usage
One-time, from a workstation with sudo ssh to the targets:
./script/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub
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:
- deploy (default, and what a push does) — apply, then validate.
- validate — check only, change nothing. Asserts each unit is active, the
installed binary matches the pinned version, the node's P2P and miner ports
are listening, peers are connected, the GPU is enumerated and drawing power,
and
miner_hashes_totalis 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.
Known conflict: GPU contention with helexa ⚠️
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:
- Put mining on a host that is not serving inference.
- Make the miner yield —
--gpu-throttle-ms, or anExecConditionthat refuses to start while a model is loaded. - Accept the contention and treat inference on that host as best-effort.
Check what a candidate miner host is already serving before enabling
quantus-miner.service on it, not after.
Deviations from house convention
runs-on: infra, notfedora-43.gitea-runners.md§4 says a deploy needs only ssh + rsync and so fitsfedora-43. That is about tooling, not routing — these targets are mesh-only.internalnames, andlair/mail's two working deploys both useinfrafor that reason. Revisit if the fedora runners gain mesh reachability.- Upstream protocol ports rather than derived ones — see Ports above.
MemoryDenyWriteExecute=falseon the node (Substrate JITs its wasm runtime through wasmtime) andPrivateDevices=falseon the miner (needs/dev/nvidia*). Both are annotated in the unit files; neither is tidy-up-able.Type=simple, notnotify—quantus-nodedoes notsd_notify(READY=1).- No Rust workspace. This repo deploys upstream release binaries; there is
nothing to build, so
generic.md§1–§2 do not apply.
Upstream
- node — https://github.com/Quantus-Network/chain (
MINING.mdis the real doc) - miner — https://github.com/Quantus-Network/quantus-miner
- guide — https://docs.quantus.com/guides/mining
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.