Documentation

MachinaHash (MH-1) — the PoW algorithm for PowMachina

Status: the calibration is closed and the algorithm is two. The CUDA kernels exist (machinahash-cuda) and are bit-exact against all 1,000 vectors on sm_86 and sm_120 alike.

Gate 2 (the B-core on a GPU) met — 12.0× against an EPYC 9474F, lane G pulls 44 % of the card’s ceiling. Gate 1 (the L-core on a CPU) not met — against a Ryzen 7950X3D the ratio came out at 0.47×, that is, a 96 GB card is faster on lane C than a desktop. Details in §7.5.

The decision (§7.6): MachinaHash-C = RandomX v2 with the PowMachina salt (CPU) · MachinaHash-G = the B-core without the L segment (GPU) · no lane H (§7.7). The lane rotation rule, the per-lane retargets, the chainwork normalization, MESS and finality do not change.

Naming: in the UI the algorithms are called MachinaHash-C and MachinaHash-G, with no additions — no “RandomX v2” and no “CPU/GPU” in brackets on screen. What is under the hood belongs here, in internal/randomx/csrc/VENDOR-RANDOMX.md and in NOTICE. MachinaHash-C is not stock RandomX: it changes the Argon2 salt, so the hashes do not match upstream (just as with RandomWOW, RandomXL or RandomARQ). RandomX’s license (BSD-3) does not reserve the name; it asks for a copyright notice with binaries too and forbids promoting our product with tevador’s or Monero’s name — hence NOTICE and the sentence saying that none of them endorses us.

After the change (§7.7): lane G does 1.12 MH/s on an RTX PRO 6000 = 65 % of the card’s ceiling, gate 2 = 17.3×, gate 1 flipped from 0.47× to ~18× (with large pages).

Sections 2.1 and 7.1–7.3 describe the L-core and a design with three lanes. That design was measured and rejected; the sections are kept as the record of what was measured and why it was not enough — not as a valid design.

1. What the algorithm has to achieve

Goal How
CPUs get a foothold their own lane with its own difficulty + a latency-hard phase where a CPU wins
GPUs get a foothold their own lane + a bandwidth-hard phase where a GPU wins
ASICs/FPGAs make no sense a chip would have to cover two opposing hardware domains at once; otherwise it earns at most 1/3 of the emission
The shares are guaranteed not by faith in a “fair” algorithm, but by consensus (per-lane difficulty + the rotation rule)

Why not “one fair algorithm”

Memory-hard PoW is always either latency-bound (the critical path = the serial latency of random accesses → CPUs win: a big cache, low latency, strong single-thread) or bandwidth-bound (many independent accesses → GPUs win: GDDR6X/HBM has 10–15× the throughput of DDR5). It is not a matter of tuning, it is a matter of what is on the critical path.

Anyone trying to hit parity with a single algorithm (Xelis: ~440 KB of scratchpad in v2, ~544 KB in v3, ChaCha8 + Blake3 + AES rounds) has to dig it up again with every new hardware generation — Xelis changed its structure three times from v1 to v3, the last time precisely in order to suppress GPU parallelism. That is proof that parity is not a state but never-ending maintenance.

MachinaHash turns that around: it does not attempt parity in the algorithm. It has two clearly separated domains and guarantees each of them a share of the emission in consensus.

2. The two cores

2.1 The L-core (the latency domain — the CPU’s domain)

S       = ChaCha8(key = BLAKE3(header || nonce || lane))  → fills sp_bytes of scratchpad
acc     = BLAKE3_XOF(S[0..64])                            → a 4× u64 accumulator
for i in 0 .. ITER_L-1:                  # serially dependent, the count is set by the lane
    idx = acc[0] mod (1 MiB / 32)        # the index depends on the previous iteration
    v   = S[idx*32 .. idx*32+32]         # a 32 B read
    acc = MIX64(acc, v)                  # add/xor/rot + a 64×64→128 widening mul
    S[idx*32 ..] = acc                   # written back (the scratchpad changes)
L_out   = BLAKE3(S)

Properties:

  • A 64 KiB – 1 MiB scratchpad, the segment picks the size. A full 1 MiB fits in the L2 of a modern x86 core (Zen 4/5: 1 MiB/core, Intel P-core: 2 MiB). The size is a segment parameter because filling the scratchpad is parallelizable work — a short L segment must pay a short fill, otherwise the fill dominates the latency mix (in the first measurement it was 62 % of lane G’s time).
  • A pointer chase — idx depends on the data just read → no ILP, no prefetch, no unrolling into SIMD. The critical path = ITER_L × (L2 latency + the ALU mix).
  • Writing back — it rules out TMTO (a time-memory trade-off) and recomputing the scratchpad from scratch.
  • A 128-bit widening mul — expensive on an FPGA, an ordinary instruction on x86/ARM.
  • A GPU suffers twice here: 1 MiB × thousands of threads does not fit in L2 (4–96 MiB) → it goes to VRAM with a latency of ~350–500 cycles, and parallelism does not help, because the critical path is serial.

2.2 The B-core (the bandwidth domain — the GPU’s domain)

DAG     = the epoch's dataset (starting at 1 GiB, +16 MiB/epoch), generated from a 16 MiB cache
mix     = BLAKE3(L_out)[0..256]                      # a 256 B mix state
for r in 0 .. ROUNDS_B-1:
    for k in 0 .. 63:                                # 64 INDEPENDENT accesses
        idx  = FNV1a(mix, r, k) mod (|DAG| / 256)
        mix  = RANDMATH(mix, DAG[idx*256 .. +256])   # mul/rot/xor/clz/popcount, seed = the epoch
    mix = BLAKE3(mix)                                # a barrier between rounds
B_out   = BLAKE3(mix)

Properties:

  • Independent 256 B accesses, the count per round is picked by the segment (64 in lane C, 256 in G and H) → purely bandwidth-bound; a GPU coalesces and parallelizes, a CPU is limited by its DDR channels.
  • A 1–8 GiB DAG — beyond any on-chip SRAM; an ASIC would need GDDR/HBM = a GPU at a worse price. It grows +4 MiB/epoch with a cap of 8 GiB (reached after ≈ 34 years): the growth keeps out an ASIC with fixed memory, the slow pace keeps the algorithm from retiring cards quickly — Ethash retired 4 GB GPUs. With an epoch of 6.9 days that is 2.0 GiB after 5 years and 3.1 GiB after 10, so a 6 GB card gets a decade of mining. Above ~4 GiB (≈ after 14.5 years) 4–6 GB cards, 8 GB laptops and SBCs drop out — that gets handled with a tweak when the time comes.
  • The RANDMATH sequence is seeded by the epoch (the ProgPoW/KawPow principle) — the order of operations changes every epoch, so a hardwired datapath has nothing to optimize.
  • Light verification: a DAG item is recomputed from the 16 MiB cache (the Ethash model). But it is expensive: the cost grows linearly with the number of items read, and in F2 light verification of lane G came out at 101 ms/block with 8,192 items. Hence: lane G is capped at 4,096 items (1 MiB) per hash and the full DAG is held by a miner and by a node that is catching up on history (≈1 GiB of RAM, verification ~1 ms). A node at the tip verifies light (-dag=auto): a block every 2 min in ~63 ms with 28 MiB instead of 1,052 MiB (measured, mainnet epoch 0). An initial sync entirely on the light path (-dag=light) takes ~2 h per year of history. Both paths call the same item function (Cache.datasetItem) and give a bit-identical result — TestLightAndFullDagAgree, TestLightVerifierMatchesFull.

3. The lanes — where the guarantee comes from

A block declares lane ∈ {C, G} in its header. The lane determines which algorithm is computed — not the ratio of rounds of one algorithm.

Lane Algorithm A miner’s memory Optimal hardware Share of blocks
C MachinaHash-C — RandomX v2, the PowMachina salt a 2.1 GiB dataset (fast) CPU 1/2
G MachinaHash-G — the B-core over the epoch DAG, 16 × 256 accesses a 1 GiB DAG (growing) GPU 1/2

The PoW preimage is the same for both: header(80) ‖ nonce LE(8) ‖ lane(1), that is 89 bytes with the nonce at offset 80. Lane C hands it to RandomX directly — the same pattern rx/scash and rx/pcoin use over the Bitcoin header. A miner therefore needs to know a single thing: where the nonce is.

The guaranteed share of the emission does not come from the algorithm but from consensus. Each lane has its own LWMA, so its difficulty settles at half the blocks regardless of how much hashrate it has. The work is normalized across the lanes (§5) so that an attacker cannot “buy” chainwork in the cheaper one. That is why a core can be swapped without touching any of this — which is exactly what happened in the move to RandomX.

The lane is part of the preimage, so a nonce found in one lane is useless in the other.

There is no hybrid CPU+GPU lane. A lane H was designed and measured; it had a single purpose — for a balanced rig to beat the same hardware split into C+G. With RandomX that is unachievable in principle: as the CPU half RandomX is so heavy that the GPU half comes for free next to it, and the tandem came out 11× worse than C and G separately. The details are in §7.7.

4. The lane rotation rule (a defence against 51 % on the weakest lane)

The classic weakness of multi-algo coins: an attacker picks the lane with the lowest hashrate and reorganizes the chain on their own. Hence:

A valid block must not have the same lane as both of its two direct predecessors. (At most 2 blocks of the same lane in a row.)

The genesis does not count towards the run. Its lane is a constant written into the binary, not the result of mining — if it counted, which lane may mine the network’s first blocks would be decided by a number we made up — a CPU miner would mine one block and then wait for a GPU.

The consequence: a longer reorg is not possible with one domain — an attacker needs both lanes, that is both CPU and GPU capacity.

With two lanes the rule is harder than with three: after two blocks of the same lane a single option is left, not two. And it matters more than it did: lane C is RandomX, so rented hashrate can be pointed at it (Monero has orders of magnitude more, NiceHash sells it). An attacker with rented CPUs makes at most two blocks in a row and then has to deliver lane G — and that they cannot rent.

The liveness fallback: if the gap between the block and its parent exceeds 900 s, the rule is not applied to that block (otherwise one lane dying out completely would stop the chain).

The gap is measured against the parent block’s time, not against median time past. MTP is the median of eleven blocks, so it trails the tip by roughly six of them (~12 minutes at the 120 s target the network had then, ~3 at today’s 30 s) — measured against MTP, a perfectly healthy chain satisfies the relief almost always and the rule is switched off nearly all the time. That is not a theoretical concern: a test network running with the relief measured against MTP produced 31 lane C blocks in a row.

5. Chainwork across the lanes

A naive 2^256/target does not work — the lanes’ difficulties are not on the same scale and an attacker would pick the one where “work” is cheapest to buy. We use normalization by a geometric mean (the DigiByte MultiShield model):

W_block(lane) = (2^256 / target_lane) * k_lane
k_lane        = geomean(avg_work_C, avg_work_G) / avg_work_lane

where avg_work_* is the average over the last 1,440 blocks of that lane, k_lane is recomputed once per epoch (5,000 blocks) and is deterministically derivable from the chain (no external input). The effect: one block from each lane weighs the same.

With two lanes the geometric mean is an integer square root, that is big.Int.Sqrt — deterministic and with no code of our own. The cube root, which the project had to write itself, went away along with lane H.

6. The difficulty retarget

The rules are in the specification §3.6; in short:

  • Per lane, against the chain’s clock: an exponential retarget in the style of aserti3-2d with a half-life of 1 h, recomputed at every block. A silent lane gets easier at the speed of real time, because the other lane keeps the clock moving.
  • The target time: 60 s per lane (two lanes, a 30 s network block).
  • One gap moves a lane by at most 16× either way, never below the network’s floor.
  • The timestamp: not before the parent’s (the same second is allowed), at most 30 s into the future of the validating node’s clock.

7. Calibration — MANDATORY before the parameters are fixed

mh-bench measures it; the key metric is not the hashrate but how the time breaks down across the segments. The conditions:

  1. lane C: a CPU/GPU ratio of ≥ 8× in the CPU’s favour (per socket vs per card),
  2. lane G: a GPU/CPU ratio of ≥ 8× in the GPU’s favour,
  3. lane H on a balanced rig beat the same hardware split into C+G by at least 20 % — gone along with lane H (§7.7): with RandomX as the CPU half the GPU half comes for free, so the tandem came out 11× worse than C and G separately. It was not close, it was a matter of principle.
  4. the time of one hash was in a range stratum can live with (the target: 0.5–2 ms for lane C).

The hardware we have for it: an EPYC 9474F (48C/96T, Zen 4, 12-channel DDR5) and an RTX 4080 (716 GB/s). At least one ordinary desktop (6–8 cores) and one older GPU (an RTX 3060 / RX 6600) are to be added, otherwise we set the parameters by the extremes.

Without 1–2 being met there is no reason to separate the lanes and the whole model loses its point. That is exactly what happened — gate 1 failed and lane C was swapped for RandomX (§7.5–7.6). The lane structure survived it unchanged, which was the whole point of describing it separately from the cores (SPEC §8).

7.1 The F2 results — three lanes and the L-core

The reference measurement: a Ryzen 9 7950X3D (Zen 4, 16C/32T, AVX2, 1 MiB L2/core, 3D V-Cache), a 1 GiB DAG = the mainnet size, on the RTX PRO 6000 host.

lane H/s (1 thr.) H/s (16 thr.) ms/hash latency bandwidth fill
C 570 8,427 1.75 80 % 12 % 7 %
G 1,622 19,635 0.62 1 % 97 % 1 %
H 1,026 14,831 0.97 52 % 33 % 14 %

The gate (the CPU part): 3/3 met — lane C is latency-bound (80 % ≥ 75 %), lane G is bandwidth-bound (97 % ≥ 80 %), lane C is 1.75 ms (the window is 0.5–2 ms). The sum of the segments matches the real hash (±6 %).

Lane C’s margin is thin. Repeated runs on the same machine give a latency of 75–80 % (a median of ~78 %) against a threshold of 75 %, and a hashrate of 558–584 H/s. The gate passes, but one run touched the boundary. If the lower end of that range is confirmed elsewhere too, the fix is to reduce lane C’s B segment from 6 rounds to 5 (the light penalty drops from ~3.7× to ~3.3×).

The L-core, ns per mix iteration by scratchpad size: 64 KiB → 4.35 · 256 KiB → 4.79 · 512 KiB → 5.44 · 1 MiB → 6.87. The B-core (cold accesses): 1.71 GB/s per thread, 20.7 GB/s on 16 threads; one DAG item costs ~150 ns — that is DRAM latency, not throughput.

Light verification (without the full DAG): lane C 6.63 ms · lane H 7.35 ms · lane G 13.40 ms.

A measurement trap. A benchmark that measures the segments with a fixed seed has the B-core reading the same DAG items over and over, and they sit in cache after the first round. The result is optimistic everywhere there is a lot of DAG work: lane G showed 90 % bandwidth (really 97 %), lane H 73 % latency / 8 % bandwidth (really 52 % / 33 %), the B-core ceiling 120.8 GB/s (really 20.7 GB/s). With lane C’s 64 items the error did not show. It was uncovered by the discrepancy between the sum of the segments and the real hash; the benchmark therefore rotates the seed and warns about a discrepancy > 10 % itself. The numbers here are from the rotating-seed benchmark.

The first round (a QEMU VM, 8 vCPUs without AVX2) gave C 2.65 ms / 82 % latency, G 0.85 ms / 84 % bandwidth, a B-core ceiling of 36.9 GB/s. It served for the profile, not for absolute numbers — and it is exactly where the bugs described below were found.

What the measurement implied and what changed:

  1. The original parameters gave lane G a profile of 62 % fill / 4 % DAG — a GPU lane that does not work with memory. The fix: the scratchpad size is now a segment parameter (lane G has 64 KiB) and the B segment has 256 accesses per round instead of 64.
  2. Light verification of lane G came out at 101 ms/block → lane G was capped at 1 MiB of DAG per hash and a full node holds the full DAG (see §2.2).
  3. Settled by measurement: lane C ran at 2.65 ms on the VM (above the window). The parameter was deliberately not changed and on a real CPU with AVX2 it came out at 1.52 ms — so calibrating by a QEMU VM really would have been a mistake.
  4. Lane H is not fill-bound on real hardware: 38 % on the VM → 18 % on Zen 4 (ChaCha8 with AVX2). It remains open whether moving the fill onto the GPU pays off with a pipeline — F3 decides that.

7.2 What the Zen 4 measurement opened up for F3

Big L2/L3 caches shrink the penalty for a 1 MiB scratchpad. On the VM the mix latency rose from 64 KiB to 1 MiB by 83 % (5.93 → 10.87 ns), on the 7950X3D by 58 % (4.35 → 6.87 ns) — 1 MiB fits in a core’s L2 and, on top of that, in the 3D V-Cache. That means GPUs with a big L2 (Ada 96 MB, Blackwell 128 MB) may be better at the L-core than the design assumes. F3 has to measure that directly; if it is confirmed, the response is to grow lane C’s scratchpad beyond the capacity of a GPU’s L2 per thread, not to add iterations.

The B-core ratio looks safe. With cold accesses a CPU manages 20.7 GB/s on 16 threads, because it is limited by DRAM latency (~150 ns per item), not by throughput. A GPU parallelizes thousands of accesses at once over 936 GB/s (an RTX 3090) up to ~1.8 TB/s (an RTX PRO 6000). The original worry that the ≥ 8× gate might fail came from the distorted figure of 120.8 GB/s. Only the GPU kernel can confirm it.

7.3 Lane C without the DAG — the hole and its limits

The hole: lane C read only 64 DAG items per hash, so it could be mined without the DAG — with just the 16 MiB cache, from which the items are recomputed. The light path was only ~1.5× slower. Lane C could therefore be mined on devices with nowhere to store a DAG at all: routers, IoT, a browser. Since lane C has a guaranteed third of the emission, that is exactly the kind of hashrate we do not want.

The fix: lane C reads 1,536 items (6 rounds × 256). The measurement for real (a rotating seed, Zen 4):

rounds items hash time light penalty latency
1 (originally) 256 1.58 ms 1.5× 89 %
4 1,024 1.70 ms 2.9× 83 %
6 1,536 1.77 ms 3.7× 79.5 %
8 2,048 1.85 ms 4.4× 76 %
12 3,072 2.01 ms 5.8× 70 % ✗

The limit of this fix: while keeping lane C’s latency profile it cannot get above ~4×. One DAG item costs ~150 ns from RAM and recomputing it from the cache ~3.3 µs — so at most ~22× per item, and every extra item pushes lane C from latency towards bandwidth. 4× combined with the weak CPUs of such devices is enough to drop out routers, IoT and the browser. It does not stop a botnet on ordinary PCs — those simply load the 1 GiB DAG.

A stronger penalty is possible, but outside lane C: raising the cost of recomputing an item (DAG_PARENTS 256 → 1024) would make the light path ~4× more expensive with no effect on the full DAG. It would, however, touch every lane and every vector, slow down building the DAG (~1 s → ~4 s per 1 GiB) and light verification for SPV clients (lane G ~13 → ~54 ms). Not done — an open option.

What the algorithm will never solve: a botnet on ordinary PCs with enough RAM, and cloud cryptojacking (stolen credits). Lane C has a guaranteed third of the emission — if botnets take it over, they get it. The decision to leave lane C its guaranteed share was taken knowingly, with that risk.

7.4 What is left to measure

Measurement Where Status
a CPU with AVX2, 16 cores (Zen 4) the 7950X3D done — §7.1
an EPYC 9474F, 12-channel DDR5 a 48-core Zen 4 host done — §7.5
an RTX 3090 (936 GB/s) a dedicated host done — §7.5
an RTX PRO 6000 Blackwell (1,792 GB/s) a dedicated host done — §7.5
an older GPU (a 3060 / RX 6600) to be added open — but the gate already decided without it (§7.6)

7.5 The F3 results — the gate decided

The CUDA kernels are in machinahash-cuda/, bit-exact against all 1,000 test vectors on both cards (sm_86 and sm_120). The mainnet profile, a 1 GiB DAG.

lane C lane G lane H
EPYC 9474F (1 socket, 96 thr.) 19,502 62,477 38,062
Ryzen 9 7950X3D (16C/32T) 8,427 19,635 14,831
RTX PRO 6000 (1,792 GB/s) 18,092 749,053 30,886
RTX 3090 (936 GB/s) 6,958 260,544 9,370

Gate 2 (the B-core ≥ 8× in the GPU’s favour): MET. 6000/EPYC = 12.0× · 6000/Ryzen = 38.2× · 3090/Ryzen = 13.3× · 3090/EPYC = 4.2×. On the 6000 lane G pulls 785 GB/s, that is 44 % of the card’s theoretical ceiling; on the 3090 273 GB/s (29 %).

Gate 1 (the L-core ≥ 8× in the CPU’s favour): NOT MET, and not narrowly. EPYC/6000 = 1.08× · Ryzen/6000 = 0.47× (the card is 2.1× faster) · Ryzen/3090 = 1.21×.

Why the L-core did not hold up against a GPU

The L-core’s only weapon is a 1 MiB scratchpad with a serial dependency. A GPU beats that with parallelism:

CPU:  96 cores / 4 ns (L2)       =  24 accesses/ns
GPU:  (96 GB / 1 MiB) / 400 ns   = 240 accesses/ns

It cannot be saved by the scratchpad’s size. For a CPU to win 8× against a 96 GB card, the scratchpad would have to be ≈ 80 MB — and that fits in no cache, so the CPU would be DRAM-bound too and the advantage would vanish. Against a 24 GB card it comes out at 20 MB, the same problem.

Three asymmetries remain at the level of operations, and only one of them is big:

lever effect in the L-core
data-dependent branching (a program per hash) warp divergence, up to 32× no
AES-NI a CPU does 1 round/cycle, a GPU ~20× slower in software no
floats with exact rounding a GPU emulates them no

The measurement shows why AES alone is not enough: on lane C a GPU runs at 0.6 % of its ALU capacity (it is bound by memory latency), so it would absorb added arithmetic entirely. Branching is the only thing that breaks the SIMT model the card stands on.

Why chaining more CPU algorithms does not help

The CPU/GPU ratio is T_gpu / T_cpu and chaining adds the times on both sides, so the result is a weighted average of the components — it never exceeds the best one:

composition (the same CPU time for each part) CPU/GPU
RandomX-like only (branching + AES) 6.0×
RandomX-like + our L-core 3.5×
RandomX-like + Argon2-like 3.8×
all three 2.9×

Every weaker component dilutes the ratio. The right move is to give the best construction 100 % of the lane.

7.6 The decision: two named algorithms

content status
MachinaHash-G the B-core as it is — the DAG, RANDMATH, the FNV mix stays, the gate is met
MachinaHash-C RandomX with PowMachina’s own configuration replaces the L-core
lane H a tandem of MachinaHash-C + MachinaHash-G by the same model

Why RandomX and not our own VM: “a program per hash with branching” is a virtual machine. Writing our own means a cryptographic design from scratch, our own audit and years of trust — and it would end up very similar, only unvetted.

What of it is still ours:

  • our own parameters (the scratchpad, the program length, the instruction frequencies). There is precedent — xmrig supports rx/wow, rx/arq, rx/sfx and other variants.
  • our own key schedule — a RandomX key changes periodically, which fits exactly onto the rotation every 130,000 blocks from §8.
  • the lane architecture with a guaranteed third of the emission for each domain — that is the part nobody else has, and a change of core does not touch it.

What does NOT change: the lane structure, the per-lane LWMA, the lane rotation rule, the chainwork normalization, MESS, finality, the whole of powmachinad. That is exactly why the lane architecture and the cores are described separately in this specification (SPEC §8).

7.7 The results with two algorithms

The change was made and measured. Lane C = MachinaHash-C (RandomX v2), lane G = MachinaHash-G without the L segment, no lane H.

Lane G is 50 % faster without the L segment

Removing the L segment threw the ChaCha8 fill and the 64 KiB per-thread scratchpad out of the GPU kernel. The scratchpad was 11 % of the memory traffic — and, above all, what capped the number of concurrent hashes on a 24 GB card before the hashrate stopped rising.

with the L segment without it
RTX PRO 6000 749,053 H/s · 44 % of the ceiling 1,115,844 H/s · 1,170 GB/s · 65 % of the ceiling 1.49×
RTX 3090 260,544 H/s · 29 % of the ceiling 390,867 H/s · 410 GB/s · 44 % of the ceiling 1.50×

A thread now needs only 65 words of shared memory, so VRAM holds the DAG alone.

The CPU (an EPYC 9474F, 1 socket, 96 threads, a 1 GiB DAG)

H/s ms/hash
lane C, W^X on 12,006 1.96
lane C, W^X off 25,112 —
lane G 64,515 (67.7 GB/s) 1.06

Light verification (a node without the full datasets): lane C 24.9 ms, lane G 98.0 ms.

The gate

result
Gate 2 — lane G, GPU/CPU ≥ 8× 6000/EPYC = 17.3× ✅
3090/EPYC = 6.1× against the hardest CPU reference
Gate 1 — lane C, CPU/GPU ≥ 8× EPYC/RTX 4090 = 7.4× without large pages ⚠
the estimate with large pages ≈ 18.5× ✅ once configured
Gate 4 — lane C within the 0.5–2 ms window 1.96 ms ✅

For comparison: the original L-core had gate 1 at 0.47× — the card was faster than the desktop. The change therefore flipped the ratio by roughly 16-fold.

Two things an operator has to know

RANDOMX_FLAG_SECURE (W^X) costs 2.17× — 12,006 vs 25,112 H/s on 96 threads. On a single thread almost nothing, so it is not the cost of mprotect but a TLB shootdown across the cores. The API handles it with a role: Verifier (foreign data, W^X always) and Miner { secure } (your own template). A node pays for one hash per block, so the cost does not concern it.

Large pages have to be configured by hand. RandomX uses MAP_HUGETLB exclusively, that is, reserved vm.nr_hugepages; transparent huge pages in madvise mode are not enough for it (it does not call madvise). Without them the whole of RandomX runs on 4 KiB pages and loses ~2.5×. It is the same requirement xmrig has — but it belongs in the documentation for miners.

8. Rotation

What rotates and what does not

Interval Who has to do something
RANDMATH in the B-core + DAG growth (lane G) every epoch, 5,000 blocks ≈ 6.9 days nobody, it is derived from the height
The RandomX key (lane C) the same epoch nobody, it is derived from the height

An epoch’s key is BLAKE3(DOMAIN_RX_KEY ‖ epoch_LE). It is derived from the height alone, not from the hash of an older block as in Monero. The dataset is public and deterministic anyway, so deriving it from the chain gains nothing — and it removes the question of what to do about a reorg across an epoch boundary. A miner can produce the key arbitrarily far in advance.

No scheduled rotation of the CPU algorithm

The L-core design changed its mix variant every 130,000 blocks and required a new binary. That had a single purpose: abandoned mining malware has nobody to update it, so it dies out. Monero ended 72 %, 89 % and 96 % of campaigns that way in 2018–19 (IMC 2019).

With RandomX that stops making sense from two directions:

  1. The mechanism would not work. Rotation only works when a miner has to obtain new code. With RandomX that would mean a new algorithm ID in xmrig every six months — nobody maintains that and miners would rightly see it as harassment.
  2. The replacement is better. RandomX asks for 2.1 GiB per miner in fast mode. That is a memory barrier the L-core with its 1 MiB scratchpad did not have at all. A botnet cannot inconspicuously ask for 2 GiB on somebody else’s computer, and light mode is orders of magnitude slower.

Monero, incidentally, rotated its PoW exactly as long as it did not have RandomX. Rotation was a substitute for the resistance it lacked, not an addition to it.

Changing the PoW by a hard fork remains an option — it just is not planned in advance. The signalling bits in the header’s version have existed for that since genesis (SPEC §2.4).

9. The reference implementation

Status: it exists for both lanes.

where verification
MachinaHash-C internal/randomx/ (+ csrc/randomx/) RandomX v2 pinned to 615de64, the only change is the salt (internal/randomx/csrc/VENDOR-RANDOMX.md in the repository); randomx_conformance_test.go verifies our build against the official RandomX vectors in light and fast mode alike
MachinaHash-G internal/mh/bcore.go, dag.go 1,000 test vectors
The GPU (lane G) machinahash-cuda 500 lane G vectors bit-exact on sm_86 and sm_120
The node cmd/powmachinad it calls internal/mh directly — no second implementation
  • One implementation of each algorithm. MachinaHash-C is the vendored canonical RandomX — one of our own must not be written, not even in Go. MachinaHash-G is Go and both the node and the miner call it directly. → A consensus disagreement between two implementations cannot arise, which is the most common way a new chain dies on a fork.
  • The GPU kernel does not get the RANDMATH sequence by computing it, but as a constant from the host. KISS99 in two implementations would be exactly the one-constant difference that splits a network — and somebody would only notice it at an epoch change.
  • No floats in our code. (RandomX does use floats, but with firmly defined rounding — that is part of its specification and of its audits.)

10. Known risks

Risk Impact Mitigation
Rented RandomX hashrate on lane C a 51 % attack — Monero has orders of magnitude more hashrate and NiceHash sells it the lane rotation rule (§4): an attacker with rented CPUs makes at most two blocks in a row, then has to deliver lane G, which they cannot rent. On top of that MESS and the finality quorum (security). It is the price of not having invented our own VM.
Botnets in lane C a guaranteed 1/2 of the emission for stolen hardware RandomX asks for 2.1 GiB per miner in fast mode — a botnet cannot inconspicuously ask for that on somebody else’s machine; light mode is orders of magnitude slower
A bug in RandomX’s JIT the node executes generated machine code over data from foreign blocks RANDOMX_FLAG_SECURE (W^X) for the Verifier role, the vendored commit pinned after the JIT bug fix from v2.0.1 (internal/randomx/csrc/VENDOR-RANDOMX.md in the repository)
GPUs handle lane G better than we expect — that is the intent; gate 2 is 17.3× (§7.7)
51 % on one lane a reorg the rotation rule, §4
No pool supports MachinaHash nobody mines it lane C is RandomX, so a variant in xmrig is enough (precedent: rx/scash, rx/pcoin); lane G needs its own stratum — it is in the roadmap, not an afterthought (the lesson from KRX)
Calibration by 3 machines the parameters are hit for an EPYC + a 6000 + a 3090, not for ordinary hardware widen the test hardware (§7.4)

11. Sources / references