Rob Thijssen 57a3705de4 feat: per-host inner_hashes, and document the wormhole scheme
The reward preimage is static and published in every block header, so two
nodes sharing one are publicly and permanently identifiable as the same
operator. That silently defeats the point of running nodes at separate
sites, so the inner_hash moves out of a single env: value and into the
deploy-node matrix, one per host, alongside the miners each node serves.

doc/wormhole-rewards.md records the derivation chain
(secret -> Poseidon2(salt|secret) = inner_hash -> Poseidon2(inner_hash) =
address) with source citations, and the analysis: publishing the
inner_hash is not a theft risk because spending needs a plonky2 proof of
knowledge of the secret, but it does make mining income permanently
attributable. The privacy in this design is at the exit, not at receipt,
which is not what MINING.md's wording suggests.

It also records the operational flaw: the inner_hash cannot be derived
from any public material and the Android wallet does not expose it, so a
seed phrase must reach some machine to produce a value that is public
anyway. Mitigation is a dedicated mining wallet, not carefulness.
2026-08-31 17:53:30 +03:00

quantus

Deployment for a Quantus Network Planck node and its external GPU miner.

Two components, two hosts:

Component Host Role
quantus-node bob.hanzalova.internal consensus, P2P, reward address. Disk + network bound.
quantus-miner benjy.hanzalova.internal Poseidon2-over-Goldilocks search on 1× RTX 4090. Pure GPU compute.

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 (beast, ×2) 368 MH/s each 600 W 0.60 MH/s/W
RTX 5090 @ 400 W floor 281 MH/s 400 W 0.70 MH/s/W
RTX 4090 (benjy) 183 MH/s 450 W 0.41 MH/s/W
Ryzen 9 9950X3D, 32t 4.15 MH/s — —
Xeon E5-2620 v3, 12t (bob) 0.50 MH/s — —

A single 5090 at its minimum power limit out-hashes every CPU in the fleet combined by roughly 24×. CPU mining is therefore disabled everywhere (--cpu-workers 0), including on the GPU hosts, where it would add ~1% hashrate for a couple hundred watts. The old Xeons 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
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.

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_total is advancing.

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: helexa on benjy ⚠️

benjy already runs helexa neuron on :13131 (architecture/generic.md §11, inference.md), serving inference from the same RTX 4090 this miner will saturate. They will contend directly: the miner drives the GPU to ~95% utilisation and ~450 W continuously, which will make inference latency on benjy by turns awful and unpredictable.

Nothing here resolves that, and the deploy does not try to arbitrate. Options, in rough order of preference:

  1. Move mining to beast (2× 5090, and 4× the hashrate) and leave benjy to inference. Requires checking what beast's neuron is doing first.
  2. Make the miner yield — --gpu-throttle-ms, or an ExecCondition that refuses to start while a model is loaded.
  3. Accept the contention on benjy and treat inference there as best-effort.

Decide before enabling quantus-miner.service on benjy, not after.

Deviations from house convention

  • 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.
  • Upstream protocol ports rather than derived ones — see Ports above.
  • 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).
  • No Rust workspace. This repo deploys upstream release binaries; there is nothing to build, so generic.md §1–§2 do not apply.

Upstream

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.

S
Description
No description provided
Readme
2 MiB
Languages
Python 53.9%
Shell 41.5%
Go Template 4.6%