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.gocomputes 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.goverifies 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.goruns the official BIP340 vectors including the edge cases (a point off the curve,sequal to the order,sG−ePat 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 -9test (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-rpcis unlocked from startup until the process exits (the password comes from the environment).walletpassphrase/walletlockwith 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.