Documentation

What to verify in this code

For anyone considering mining it, listing it or running a node — and for their tools. The list is written to be ticked off, not to persuade.

1. The trust assumptions — the complete list

Premine none. The genesis coinbase is unspendable
An admin key in consensus none. Nothing can be changed without a new binary and a hard fork
Dev fee 5 % of the subsidy up to block 525,000 (≈ 2 years), then 0. Enforced by consensus, visible in every coinbase
The finality quorum the only trust assumption. 5 of 9 signatures; it can only confirm a block the node verified itself, and it can be switched off (--no-finality) — then it is pure PoW
External dependencies five direct ones: pebble (storage), blake3 (the hash), btcec (Schnorr/secp256k1), golang.org/x/crypto (Argon2id, XChaCha20-Poly1305 — the wallet), golang.org/x/term (a password without echo) — 37 modules in the binary in total, 33 of them pulled in by Pebble. Plus the vendored RandomX v2 in the tree (internal/randomx/csrc/), not as a dependency
RandomX (lane C) vendored, pinned to commit 615de64, the only change is the salt — internal/randomx/csrc/VENDOR-RANDOMX.md. Our build is verified against the official RandomX vectors
The JIT in RandomX the node executes machine code generated at runtime over data from foreign blocks. This is mitigated by RANDOMX_FLAG_SECURE (W^X) for the Verifier role and by pinning past the JIT bug fix from v2.0.1
Pebble the storage of blocks and UTXOs. The atomicity of its batches is an assumption that crash consistency rests on (internal/store)
The optional indexes -txindex and -addrindex write into the same batch as the block and are unwound together with it on a reorg. It is not a separate database, so no difference can arise between the chain and the index — which is exactly what breaks explorers built on their own storage

If you find anything else that requires trust in the operator, that is a bug — report it.

2. What you can verify in a few minutes

go test ./...                                     # consensus, 1,000 test vectors, the storage kill -9
go test -tags mh_stock_salt ./internal/randomx/   # our RandomX build against the OFFICIAL v2 vectors
  • The emission: internal/chain/subsidy_test.go computes the whole emission curve and checks the cap. What is actually emitted is 83,900,029 POWM — less than the 84 M cap, because the slow start takes off ~100,000.
  • The dev fee: the same test sums the whole volume (1,045,001 POWM) and verifies that it is zero above height 525,000.
  • The algorithm’s stability: internal/mh/mh_test.go verifies 1,000 vectors. Those vectors were created over the previous core in Rust and survived the rewrite into Go unchanged — that is exactly what they are for. If the node and the miner diverged by a single bit, this test would fail.
  • Schnorr: internal/mh/schnorr_test.go runs the official BIP340 vectors including the edge cases (a point off the curve, s equal to the order, sG−eP at infinity).

3. Where to look for holes (and what we have already found there)

Area Status
Floats in consensus none. MESS and chainwork are both integer; MessMultiplier returns a float, but only for logging — the decision is made by AcceptsReorg
Crashes on invalid input 5 fuzz targets (the header, the compact target, the retarget, attestations, MESS), they run in CI too
Reward arithmetic int64 in cogs; the maximum intermediate result is ~2·10¹³, far from overflowing
The integer root (chainwork) tested on r³ ≤ x < (r+1)³ and on exact powers up to 2^2200
unsafe in Go only the cgo boundary to RandomX (internal/randomx) — passing a pointer to a buffer and its length. The algorithm itself is pure Go
Anti-DoS limits in P2P framing, the addrman, the peer manager, deadlines; a ban is keyed by machine (an IPv4 address, an IPv6 /64), connection limits per machine/network before the handshake, a message and byte limit per peer, an immediate ban for bad PoW, a deep fork rejected before the PoW, a ban export for the firewall — security, §5
Dust spam storage mass (KIP-9) in consensus, a per-block cap including the coinbase; saturating integer arithmetic, an output with zero = infinite mass — internal/chain/storagemass.go
Canonical serialization (tx, block, header, messages) fuzzed, a round trip has to give the same bytes
Signature verification done — Schnorr (BIP340) through btcec, one implementation in internal/mh; verified by the official BIP340 vectors. The sighash commits to the amounts and the scripts of all the inputs

What the fuzzing found

The difficulty floor was not actually enforced. BigToCompact rounds up, so clamping to the floor before encoding could push the result 0.06 % above the boundary. And with a short history the retarget returned the bits from the last header with no check at all — so it would blindly repeat a value from a foreign peer. Both are fixed (clampBitsToFloor, the single place where the floor is enforced), and both cases are in the seed corpus as a permanent regression test.

4. What is deliberately boring

  • No AI, no “useful work”, no stateful VM. Pearl showed that “useful work” cannot be verified; a chain we have watched closely showed that an AI layer breaks stability.
  • No holder reward and no staking — the reward goes for work, not for holding. Why not even a Decred-style hybrid (and what stayed open from it) → security, §3, “Staking — considered, rejected”.
  • Zero dependencies in the Go node fell through — deliberately, because of the storage. A durability bug means a silent loss of money, not a crash somebody notices; a proven LSM tree is a smaller risk there than our own code. Crash consistency has a real kill -9 test (internal/store/crash_test.go), not a simulated one.
  • The chain’s growth is capped: 96 kB / 30 s ≈ 101 GB/year of transactions at full utilization, plus the settlement bodies, which a full node prunes once final. While the chain is quiet, the blocks themselves are ~323 MB/year and a node on disk ~736 MB/year (SPEC §2.2). No witness discount, so storing data on the chain does not pay.

5. What is expected of you as a miner

Nothing regular. There is no scheduled rotation of the CPU algorithm (ALGO §8): it only works by forcing a miner to obtain a new binary, and with RandomX it would mean a new algorithm ID in xmrig every six months.

What changes by itself and without your intervention: the RandomX key (lane C) and the DAG with its RANDMATH sequence (lane G) every epoch, that is, 5,000 blocks ≈ 6.9 days. Both are derived from the height, so a miner can produce them in advance.

What you do have to set up yourself: large pages (vm.nr_hugepages). RandomX uses MAP_HUGETLB exclusively and without reserved pages it loses ~2.5× of its performance. It is the same requirement xmrig has.

6. Known weak spots

They are in the documentation too, they are not hidden:

  • Lane C has a guaranteed half of the emission, botnets included. RandomX asks for 2.1 GiB per miner in fast mode, which is a barrier the L-core did not have — but nothing in the algorithm stops a botnet on an ordinary PC with memory to spare (security, the botnet research).
  • Rented RandomX hashrate can be pointed at lane C. Monero has orders of magnitude more and NiceHash sells it. This is covered by the lane rotation rule (security, §1: an attacker needs

    2/3 of the honest lane G regardless of lane C), by MESS and by finality. That inequality has not been verified by a third party.

  • MESS is a subjective rule. With two partitions in almost perfect balance there is a risk of a permanent bifurcation — which is why it only acts beyond the boundary where reorgs are rare.
  • Gate 1 only passes with large pages (7.4× without them, ~18.5× with them; ALGO §7.7).
  • An invalid branch is marked, but the node does not ban whoever sent it. The body matched the Merkle root of a verified header, so the peer could not have known it was invalid (that is only found by validating against the UTXO set). An attacker can therefore produce such a branch again — they always pay a block of work for it and it costs us one downloaded body, because the mark survives a restart (chain/invalid.go, synchronisation).
  • The finality quorum is a concentrated risk if the keys were not held by independent people.
  • The reserved script versions 1–15 are spendable by anyone today. That is the price of the soft-fork path to contracts (SPEC §2.4) — but it means nobody should send funds to such an output until the version is activated.
  • The node holds no keys. The wallet is a separate process (powmachina-wallet-cli, powmachina-wallet-rpc) that scans the blocks itself — the node never learns which addresses belong to whom. The file is always encrypted: Argon2id (256 MiB, 3 passes) + XChaCha20-Poly1305, with the whole header as AAD (the wallet). There is no unencrypted mode.
  • The wallet in wallet-rpc is unlocked from startup until the process exits (the password comes from the environment). walletpassphrase/walletlock with a time limit is not there yet — whoever reaches the port and has the token can sign a withdrawal.
  • A BIP39 passphrase is not supported and our own implementation would have to be ASCII — BIP39 normalizes the passphrase through Unicode NFKD and we do not write that ourselves.
  • Imported keys are not covered by the 24-word backup — the CLI says so on import.
  • Addresses carry a checksum: they are bech32m (BIP350) with the network’s prefix, a single typo invalidates an address and an address from another network is rejected.
  • The wallet runs in the same process as the node. In production the keys belong elsewhere.
  • The mainnet genesis does not exist — on mainnet the node refuses to run, so that a placeholder cannot become the root of trust by mistake.